From 713c9ac4ef37eace83c0243ebc4eaf2bf35d65c7 Mon Sep 17 00:00:00 2001 From: Carl Colglazier Date: Tue, 21 May 2024 14:22:11 -0500 Subject: [PATCH] Break code into more files. --- code/recc/__init__.py | 2 + code/{scratch => recc}/build_suggestion.py | 37 +++- code/{scratch => recc}/federated_design.py | 4 +- code/scratch/__init__.py | 0 recommender/app/page.tsx | 11 + recommender/app/witch/page.tsx | 193 ------------------ recommender/components/SensativeSelector.tsx | 25 +++ recommender/components/TagSelector.tsx | 130 ++++++++++++ recommender/components/server_cards.tsx | 77 +++++++ recommender/components/tag_search.tsx | 51 +++++ recommender/next.config.mjs | 2 +- recommender/package-lock.json | 182 +++++++++++++++++ recommender/package.json | 1 + recommender/tailwind.config.ts | 6 +- ...nd.config.js => unused-tailwind.config.js} | 1 + recommender/utils/locales.tsx | 43 ++++ recommender/utils/math.tsx | 44 ++++ recommender/utils/other.tsx | 2 + recommender/utils/types.tsx | 41 ++++ 19 files changed, 643 insertions(+), 209 deletions(-) create mode 100644 code/recc/__init__.py rename code/{scratch => recc}/build_suggestion.py (71%) rename code/{scratch => recc}/federated_design.py (99%) delete mode 100644 code/scratch/__init__.py create mode 100644 recommender/app/page.tsx delete mode 100644 recommender/app/witch/page.tsx create mode 100644 recommender/components/SensativeSelector.tsx create mode 100644 recommender/components/TagSelector.tsx create mode 100644 recommender/components/server_cards.tsx create mode 100644 recommender/components/tag_search.tsx rename recommender/{tailwind.config.js => unused-tailwind.config.js} (87%) create mode 100644 recommender/utils/locales.tsx create mode 100644 recommender/utils/math.tsx create mode 100644 recommender/utils/other.tsx create mode 100644 recommender/utils/types.tsx diff --git a/code/recc/__init__.py b/code/recc/__init__.py new file mode 100644 index 0000000..a890c7b --- /dev/null +++ b/code/recc/__init__.py @@ -0,0 +1,2 @@ +from .build_suggestion import ReccModel, sim_from_tag_index +from .federated_design import TagData diff --git a/code/scratch/build_suggestion.py b/code/recc/build_suggestion.py similarity index 71% rename from code/scratch/build_suggestion.py rename to code/recc/build_suggestion.py index 2cd315b..50eb342 100644 --- a/code/scratch/build_suggestion.py +++ b/code/recc/build_suggestion.py @@ -1,11 +1,10 @@ from federated_design import * -from sklearn.cluster import AffinityPropagation -from sklearn.decomposition import TruncatedSVD from scipy.sparse.linalg import svds from sklearn.preprocessing import normalize import polars as pl from sklearn.metrics.pairwise import cosine_similarity from sklearn.metrics.pairwise import euclidean_distances +from sklearn.decomposition import PCA import json class ReccModel: @@ -33,10 +32,13 @@ class ReccModel: def top_tags(self): u, s, v = self.svd(k=25) tag_stuff = np.diag(s) @ v + pca = PCA(n_components=2) + tag_pca = pca.fit_transform(tag_stuff.T) + print(tag_pca[:, 0]) return pl.DataFrame({ "tag": self.tag_names, - "x": tag_stuff[-1], - "y": tag_stuff[-2], + "x": tag_pca[:, 0], + "y": tag_pca[:, 1], "variance": np.var(tag_stuff, axis=0), "count": self.tag_use_counts[self.has_info].tolist(), "index": np.arange(len(self.tag_names)) @@ -52,8 +54,8 @@ class ReccModel: }) # This one seem pretty good! -def sim_from_tag_index(index=1000): - u, s, v = rm.svd(k=50, norm_axis=0) +def sim_from_tag_index(rm: ReccModel, index=1000): + u, s, v = rm.svd(k=25, norm_axis=0) m = (np.diag(s) @ v).T pos = m[index] server_matrix = u @ np.diag(s) @@ -64,8 +66,9 @@ if __name__ == "__main__": rm = ReccModel() rm.top_tags().write_ipc("data/scratch/tag_svd.feather") rm.top_servers().write_ipc("data/scratch/server_svd.feather") - u, s, v = rm.svd(k=100, norm_axis=None) - pos_m = v.T#(v.T @ np.diag(s))#v.T# + u, s, v = rm.svd(k=50, norm_axis=None) + #pos_m = v.T#(v.T @ np.diag(s))#v.T# + pos_m = v.T @ np.diag(s) server_matrix = u#u @ np.diag(s)#u# with open("recommender/data/positions.json", "w") as f: f.write(json.dumps(pos_m.tolist())) @@ -76,4 +79,22 @@ if __name__ == "__main__": with open("recommender/data/tag_names.json", "w") as f: f.write(json.dumps(rm.tag_names.tolist())) + + top_server_tags_df = rm.tfidf.sort(pl.col("tf_idf"), descending=True).with_columns(pl.lit(1).alias("counter")).with_columns( + pl.col("counter").cum_sum().over("host").alias("running_count") + ).filter(pl.col("running_count") <= 5).drop("counter", "running_count").select(["host", "tags", "idf", "tf_idf"]).filter( + pl.col("tf_idf") > 4 + ) + top_server_tags = {} + for row in top_server_tags_df.iter_rows(named=True): + if row["host"] not in top_server_tags: + top_server_tags[row["host"]] = [] + top_server_tags[row["host"]].append(row["tags"]) + with open("recommender/data/server_top_tags.json", "w") as f: + f.write(json.dumps(top_server_tags)) + + # group by host and add descending value + #rm.tfidf.sort(pl.col("tf_idf"), descending=True).group_by("host").with_row_index() + + #rm.server_names[np.argsort(-cosine_similarity(pos_m[779].reshape(1, -1), server_matrix))].tolist()[0][0:10] \ No newline at end of file diff --git a/code/scratch/federated_design.py b/code/recc/federated_design.py similarity index 99% rename from code/scratch/federated_design.py rename to code/recc/federated_design.py index 07cc46d..2102fc0 100644 --- a/code/scratch/federated_design.py +++ b/code/recc/federated_design.py @@ -2,8 +2,8 @@ import polars as pl from scipy.sparse import lil_matrix from sklearn.metrics.pairwise import cosine_similarity import numpy as np -import textdistance -from scipy.stats import kendalltau +#import textdistance +#from scipy.stats import kendalltau import rbo import scipy diff --git a/code/scratch/__init__.py b/code/scratch/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/recommender/app/page.tsx b/recommender/app/page.tsx new file mode 100644 index 0000000..769dd8f --- /dev/null +++ b/recommender/app/page.tsx @@ -0,0 +1,11 @@ +"use client"; +import React from 'react'; +import { TagSelector } from '../components/TagSelector'; + +export default function Home() { + return ( +
+ +
+ ); +} diff --git a/recommender/app/witch/page.tsx b/recommender/app/witch/page.tsx deleted file mode 100644 index 823c9d3..0000000 --- a/recommender/app/witch/page.tsx +++ /dev/null @@ -1,193 +0,0 @@ -"use client"; -import React, { useState, useEffect } from 'react'; -//import top_tags from "@/data/top_tags.json"; -import positions from "@/data/positions.json"; -import server_matrix from "@/data/server_matrix.json"; -import server_names from "@/data/server_names.json"; -import tag_names from "@/data/tag_names.json"; - -interface Tag { - index: number; - tag: string; -} -// Read from data/top_tags.json FILE -//const tags: Tag[] = - -const selected_tags = [ - "politics", "gardening", "art", "nature", "pride", "cycling", "climate", - "programming", "dogs", "privacy", "cats", "gaming", "education", - "science", "music", "movies", "food", "books", "lgbtq", "python", - "emacs", "gay", "trans", "furry", "photography", "cooking", "literature", - "television" -]; - - -function dotProduct(a: number[], b: number[]): number { - return a.map((x, i) => x * b[i]).reduce((sum, current) => sum + current, 0); -} - -function magnitude(arr: number[]): number { - return Math.sqrt(arr.map(x => x * x).reduce((sum, current) => sum + current, 0)); -} - -function cosineSimilarity(arr1: number[], arr2: number[]): number { - if (arr1.length !== arr2.length) { - throw new Error("Arrays must have the same length"); - } - - const dotProd = dotProduct(arr1, arr2); - const magnitudeProd = magnitude(arr1) * magnitude(arr2); - if (magnitudeProd === 0) { - return 0; - } - return dotProd / magnitudeProd; -} - -function averageOfArrays(arr: number[][]): number[] { - // Get the length of the first sub-array - const length = arr[0].length; - - // Initialize an array to store the sums - const sums = Array(length).fill(0); - - // Loop over each sub-array - for (let i = 0; i < arr.length; i++) { - // Loop over each element in the sub-array - for (let j = 0; j < arr[i].length; j++) { - // Add the element to the corresponding sum - sums[j] += arr[i][j]; - } - } - - // Divide each sum by the number of arrays to get the average - const averages = sums.map(sum => sum / arr.length); - - return averages; -} - - - -const TagSelector: React.FC = () => { - //const top_tags: Tag[] = []; - const all_tags: Tag[] = tag_names.map((tag: string, index: number) => ({index, tag})) - const tags: Tag[] = all_tags.filter((tag: Tag) => selected_tags.includes(tag.tag)); - //top_tags.filter((tag) => selected_tags.includes(tag.tag)); - // State to keep track of selected tag IDs. - const [selectedTagIds, setSelectedTagIds] = useState([]); - - const [suggestedTagIds, setSuggestedTagIds] = useState([]); - - const [topServerIds, setTopServerIds] = useState([]); - - // Function to handle tag selection toggling. - const toggleTag = (tagId: number) => { - setSelectedTagIds((currentTagIds) => - currentTagIds.includes(tagId) - ? currentTagIds.filter((id) => id !== tagId) - : [...currentTagIds, tagId], - ) - }; - - function find_most_similar_tags(all_tags: Tag[], selectedTagIds: number[], positions: number[][]) { - let most_similar: Tag[] = []; - // get the average position of all selected tags - if (selectedTagIds.length > 0) { - // loop through all selected tags and get their positions - const selected_positions = selectedTagIds.map((tagId) => positions[tagId]); - for (let i = 0; i < selected_positions.length; i++) { - let tag_similarity = positions.map((row) => cosineSimilarity(selected_positions[i], row)); - let tag_rank = tag_similarity.map((similarity, index) => ({index, similarity})).sort((a, b) => b.similarity - a.similarity).map((item, index) => ({index: item.index, similarity: item.similarity, name: tag_names[item.index]})); - console.log(tag_rank.slice(0, 10)); - let topServerIds = tag_rank.slice(0, 10).map((item) => item.index); - for (let tagId of all_tags.filter((tag) => topServerIds.includes(tag.index))) { - if (!selectedTagIds.includes(tagId.index) && !most_similar.includes(tagId)) { - most_similar.push(tagId); - } - } - } - } - return most_similar; - } - - useEffect(() => { - // get the average position of all selected tags - if (selectedTagIds.length > 0) { - let selected_positions = selectedTagIds.map((tagId) => positions[tagId]); - // get the average of selected positions - let average_position = averageOfArrays(selected_positions); - // loop through each row of the server_matrix and calculate the cosine similarity - const server_similarity = server_matrix.map((row) => cosineSimilarity(average_position, row)); - const server_rank = server_similarity.map((similarity, index) => ({index, similarity})).sort((a, b) => b.similarity - a.similarity).map((item, index) => ({index: item.index, similarity: item.similarity, name: server_names[item.index]})); - setTopServerIds(server_rank.slice(0, 10).map((item) => item.index)); - - // Find the most similar tags among all tags - const tag_similarity = all_tags.map((tag) => ({t: tag, sim: cosineSimilarity(average_position, positions[tag.index])})).sort((a, b) => b.sim - a.sim); - const most_similar = find_most_similar_tags(all_tags, selectedTagIds, positions); - //tag_similarity.slice(0, 50).map((item) => item.t); - console.log(most_similar); - setSuggestedTagIds(most_similar.map((tag) => tag.index)); - console.log(suggestedTagIds); - } - }, [selectedTagIds]); - - return ( -
-
-
-

Selected

- {all_tags.filter((tag) => selectedTagIds.includes(tag.index)).map((tag) => ( - - ))} -
-
-

Categories

- {tags.filter((tag) => !selectedTagIds.includes(tag.index)).map((tag) => ( - - ))} -
-
-

Suggested Tags ({suggestedTagIds.filter((tag) => !selectedTagIds.includes(tag)).length})

- {all_tags.filter((tag) => suggestedTagIds.includes(tag.index)).filter((tag) => !selectedTagIds.includes(tag.index)).map((tag) => ( - - ))} -
-
-
-
    - {topServerIds.map((id) => ( -
  • {server_names[id]}
  • - ))} -
-
-
- ); -}; - - -export default function Home() { - return ( -
- -
- ); -} diff --git a/recommender/components/SensativeSelector.tsx b/recommender/components/SensativeSelector.tsx new file mode 100644 index 0000000..6ef24e3 --- /dev/null +++ b/recommender/components/SensativeSelector.tsx @@ -0,0 +1,25 @@ +import { Checkbox, Field, Label } from '@headlessui/react'; + +interface SensativeSelectorProps { + showSensitiveTags: boolean; + setShowSensitiveTags: (checked: boolean) => void; +} + +export const SensativeSelector: React.FC = ({ showSensitiveTags, setShowSensitiveTags }) => { + return ( +
+ + + + + + + + +
+ ) +} \ No newline at end of file diff --git a/recommender/components/TagSelector.tsx b/recommender/components/TagSelector.tsx new file mode 100644 index 0000000..2bcb1ec --- /dev/null +++ b/recommender/components/TagSelector.tsx @@ -0,0 +1,130 @@ +"use client"; +import React, { useState, useEffect } from 'react'; +import positions from "@/data/positions.json"; +import server_matrix from "@/data/server_matrix.json"; +import server_names from "@/data/server_names.json"; +import tag_names from "@/data/tag_names.json"; +import { cosineSimilarity, averageOfArrays } from "@/utils/math"; +import { ServerCards } from "@/components/server_cards"; +import type { Tag } from "@/utils/types"; +import { isSensitive } from "@/utils/types"; +//import { selected_tags, find_most_similar_tags } from '@/app/page'; +import { selected_tags, find_most_similar_tags } from '@/utils/types'; +import { SensativeSelector } from './SensativeSelector'; +import { TagSearch } from './tag_search'; + +export const TagSelector: React.FC = () => { + //const top_tags: Tag[] = []; + const all_tags: Tag[] = tag_names.map((tag: string, index: number) => ({ index, tag })); + const tags: Tag[] = all_tags.filter((tag: Tag) => selected_tags.includes(tag.tag)); + + // state + const [selectedTagIds, setSelectedTagIds] = useState([]); + const [suggestedTagIds, setSuggestedTagIds] = useState([]); + const [topServerIds, setTopServerIds] = useState([]); + const [showSensitiveTags, setShowSensitiveTags] = useState(false); + + // Function to handle tag selection toggling. + const toggleTag = (tagId: number) => { + setSelectedTagIds((currentTagIds) => currentTagIds.includes(tagId) + ? currentTagIds.filter((id) => id !== tagId) + : [...currentTagIds, tagId] + ); + }; + + useEffect(() => { + // get the average position of all selected tags + if (selectedTagIds.length == 0) { + setTopServerIds([]); + setSuggestedTagIds([]); + } else { + let selected_positions = selectedTagIds.map((tagId) => positions[tagId]); + // get the average of selected positions + let average_position = averageOfArrays(selected_positions); + // loop through each row of the server_matrix and calculate the cosine similarity + const server_similarity = averageOfArrays(selected_positions.map((pos) => server_matrix.map((row) => cosineSimilarity(pos, row)))); + //console.log(server_similarity); + const server_rank = server_similarity.map((similarity, index) => ({ index, similarity })).sort((a, b) => b.similarity - a.similarity).map((item, index) => ({ index: item.index, similarity: item.similarity, name: server_names[item.index] })); + //console.log(server_rank); + setTopServerIds(server_rank.slice(0, 25).map((item) => item.index)); + // Find the most similar tags among all tags + const tag_similarity = all_tags.map((tag) => ({ t: tag, sim: cosineSimilarity(average_position, positions[tag.index]) })).sort((a, b) => b.sim - a.sim); + const most_similar = find_most_similar_tags(all_tags, tag_names, selectedTagIds, positions); + setSuggestedTagIds(most_similar.map((tag) => tag.index)); + } + }, [selectedTagIds, all_tags]); + + return ( +
+
+
+

Mastodon Server Reccomender

+
+
+

Search

+ +
+
+

Settings

+ +
+
+ {/*
+

Languages

+ +
*/} +

Selected

+ {all_tags.filter((tag) => selectedTagIds.includes(tag.index)).map((tag) => ( + + ))} +
+
+

Categories

+ {tags.filter((tag) => !selectedTagIds.includes(tag.index)).map((tag) => ( + + ))} +
+
+

Suggested Tags ({suggestedTagIds.filter((tag) => !selectedTagIds.includes(tag)).length})

+ {all_tags.filter((tag) => suggestedTagIds.includes(tag.index)).filter( + (tag) => !selectedTagIds.includes(tag.index) + ).filter( + (tag) => showSensitiveTags || !isSensitive(tag) + ).map((tag) => ( + + ))} +
+
+
+
    + +
+
+
+ ); +}; diff --git a/recommender/components/server_cards.tsx b/recommender/components/server_cards.tsx new file mode 100644 index 0000000..9367922 --- /dev/null +++ b/recommender/components/server_cards.tsx @@ -0,0 +1,77 @@ +import server_names from "@/data/server_names.json"; +import servers from "@/data/servers.json"; + +import top_server_tags_data from "@/data/server_top_tags.json"; +import { locales } from '@/utils/locales'; + +const top_server_tags: {[server: string]: string[]} = top_server_tags_data; + +//const ServerCards: React.FC = (topServerIds: number[]) => { +interface Server { + domain: string; + description: string; + proxied_thumbnail: string; + top_tags: string[]; + languages: string[]; +} + +// map { code: "be", language: "Беларуская" } to dictionary +const localeDictionary = locales.reduce((acc, locale) => { + acc[locale.code] = locale.language; + return acc; +}, {} as { [key: string]: string }); + +console.log(localeDictionary); + +const server_data: {[id: number]: Server} = {}; +for (let i = 0; i < servers.length; i++) { + let s_name: string = servers[i].domain; + let index = server_names.indexOf(s_name); + if (index > -1) { + server_data[index] = { + domain: servers[i].domain, + description: servers[i].description, + proxied_thumbnail: servers[i].proxied_thumbnail, + top_tags: [], + languages: servers[i].languages + }; + if (top_server_tags[s_name] !== undefined) { + server_data[index].top_tags = top_server_tags[s_name]; + } + }; +}; + +export function ServerCards(props: {topServerIds: number[], tagCount: number}) { + let server_info = props.topServerIds.map((id) => server_data[id]); + return ( +
+ {props.tagCount >= 2 ? ( +
+
+ {server_info.filter((data) => data !== undefined).map((data) => ( +
+
+ +
+
+

{data.domain}

+

{data.languages.map((lang) => ( + {localeDictionary[lang]} + ))}

+

{data.description}

+
    + {data.top_tags.map((tag) => ( +
  • {tag.split('_')[0]}
  • + ))} +
+
+
+ ))} +
+
+ ) : ( +
Please select at least two tags.
+ )} +
+ ); +} \ No newline at end of file diff --git a/recommender/components/tag_search.tsx b/recommender/components/tag_search.tsx new file mode 100644 index 0000000..f3c37bd --- /dev/null +++ b/recommender/components/tag_search.tsx @@ -0,0 +1,51 @@ +import React, { useState, useEffect } from 'react'; +import { Combobox, ComboboxInput, ComboboxOption, ComboboxOptions } from '@headlessui/react'; +import type { Tag } from '@/utils/types'; + +interface TagSearchProps { + all_tags: Tag[], + selectedTagIds: number[]; + onToggle: (tag: number) => void; +} + +export const TagSearch: React.FC = ({ all_tags, selectedTagIds, onToggle }) => { + + const [tagQuery, setTagQuery] = useState('') + + const tag_options = all_tags; + + const filteredTags = + (tagQuery === '' || tagQuery.length < 2) ? + [] : + tag_options.filter((tag) => ( + tag.tag.toLowerCase().startsWith(tagQuery.toLowerCase()) + )); + + + return ( + { + if (typeof tag_id === 'number') { + onToggle(tag_id); + } + }} + onClose={() => setTagQuery('')} + > + tag?.tag}*/ + className='border data-[hover]:shadow data-[focus]:bg-blue-100 w-full' + onChange={(event) => setTagQuery(event.target.value)} + /> + + {filteredTags.map((tag) => ( + + {tag.tag} + + ))} + + + ) +}; \ No newline at end of file diff --git a/recommender/next.config.mjs b/recommender/next.config.mjs index 72bd198..265a802 100644 --- a/recommender/next.config.mjs +++ b/recommender/next.config.mjs @@ -1,7 +1,7 @@ /** @type {import('next').NextConfig} */ const nextConfig = { output: 'export', - basePath: '/files/jsdemo' + basePath: '/demos/deweb2024' }; export default nextConfig; diff --git a/recommender/package-lock.json b/recommender/package-lock.json index 180499d..2287922 100644 --- a/recommender/package-lock.json +++ b/recommender/package-lock.json @@ -8,6 +8,7 @@ "name": "recommender", "version": "0.1.0", "dependencies": { + "@headlessui/react": "^2.0.3", "next": "14.1.3", "react": "^18", "react-dom": "^18" @@ -113,6 +114,72 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.2.tgz", + "integrity": "sha512-+2XpQV9LLZeanU4ZevzRnGFg2neDeKHgFLjP6YLW+tly0IvrhqT4u8enLGjLH3qeh85g19xY5rsAusfwTdn5lg==", + "dependencies": { + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.5.tgz", + "integrity": "sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==", + "dependencies": { + "@floating-ui/core": "^1.0.0", + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.26.15", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.15.tgz", + "integrity": "sha512-WKmfLkxTwCm09Dxq4LpjL3EPbZVSp5wvnap1jmculsfnzg2Ag/pCkP+OPyjE5dFMXqX97hsLIqJehboZ5XAHXw==", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@floating-ui/utils": "^0.2.0", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.9.tgz", + "integrity": "sha512-q0umO0+LQK4+p6aGyvzASqKbKOJcAHJ7ycE9CuUvfx3s9zTHWmGJTPOIlM/hmSBfUfg/XfY5YhLBLR/LHwShQQ==", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.2.tgz", + "integrity": "sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==" + }, + "node_modules/@headlessui/react": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.0.3.tgz", + "integrity": "sha512-Xd1h0YZgfhxZ7W1w4TvK0/TZ1c4qaX4liYVUkAXqW1HCLcXSqnMeYAUGJS/BBroBAUL9HErjyFcRpCWRQZ/0lA==", + "dependencies": { + "@floating-ui/react": "^0.26.13", + "@react-aria/focus": "^3.16.2", + "@react-aria/interactions": "^3.21.1", + "@tanstack/react-virtual": "3.5.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^18", + "react-dom": "^18" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -432,6 +499,83 @@ "node": ">=14" } }, + "node_modules/@react-aria/focus": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.17.0.tgz", + "integrity": "sha512-aRzBw1WTUkcIV3xFrqPA6aB8ZVt3XyGpTaSHAypU0Pgoy2wRq9YeJYpbunsKj9CJmskuffvTqXwAjTcaQish1Q==", + "dependencies": { + "@react-aria/interactions": "^3.21.2", + "@react-aria/utils": "^3.24.0", + "@react-types/shared": "^3.23.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/interactions": { + "version": "3.21.2", + "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.21.2.tgz", + "integrity": "sha512-Ju706DtoEmI/2vsfu9DCEIjDqsRBVLm/wmt2fr0xKbBca7PtmK8daajxFWz+eTq+EJakvYfLr7gWgLau9HyWXg==", + "dependencies": { + "@react-aria/ssr": "^3.9.3", + "@react-aria/utils": "^3.24.0", + "@react-types/shared": "^3.23.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/ssr": { + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.3.tgz", + "integrity": "sha512-5bUZ93dmvHFcmfUcEN7qzYe8yQQ8JY+nHN6m9/iSDCQ/QmCiE0kWXYwhurjw5ch6I8WokQzx66xKIMHBAa4NNA==", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-aria/utils": { + "version": "3.24.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.24.0.tgz", + "integrity": "sha512-JAxkPhK5fCvFVNY2YG3TW3m1nTzwRcbz7iyTSkUzLFat4N4LZ7Kzh7NMHsgeE/oMOxd8zLY+XsUxMu/E/2GujA==", + "dependencies": { + "@react-aria/ssr": "^3.9.3", + "@react-stately/utils": "^3.10.0", + "@react-types/shared": "^3.23.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-stately/utils": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.0.tgz", + "integrity": "sha512-nji2i9fTYg65ZWx/3r11zR1F2tGya+mBubRCbMTwHyRnsSLFZaeq/W6lmrOyIy1uMJKBNKLJpqfmpT4x7rw6pg==", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@react-types/shared": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.23.0.tgz", + "integrity": "sha512-GQm/iPiii3ikcaMNR4WdVkJ4w0mKtV3mLqeSfSqzdqbPr6vONkqXbh3RhPlPmAJs1b4QHnexd/wZQP3U9DHOwQ==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, "node_modules/@rushstack/eslint-patch": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.7.2.tgz", @@ -446,6 +590,31 @@ "tslib": "^2.4.0" } }, + "node_modules/@tanstack/react-virtual": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.5.0.tgz", + "integrity": "sha512-rtvo7KwuIvqK9zb0VZ5IL7fiJAEnG+0EiFZz8FUOs+2mhGqdGmjKIaT1XU7Zq0eFqL0jonLlhbayJI/J2SA/Bw==", + "dependencies": { + "@tanstack/virtual-core": "3.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.5.0.tgz", + "integrity": "sha512-KnPRCkQTyqhanNC0K63GBG3wA8I+D1fQuVnAvcBF8f13akOKeQp1gSbu6f77zCxhEk727iV5oQnbHLYzHrECLg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -1189,6 +1358,14 @@ "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -4373,6 +4550,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" + }, "node_modules/tailwindcss": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", diff --git a/recommender/package.json b/recommender/package.json index d0d0df0..1bc036a 100644 --- a/recommender/package.json +++ b/recommender/package.json @@ -9,6 +9,7 @@ "lint": "next lint" }, "dependencies": { + "@headlessui/react": "^2.0.3", "next": "14.1.3", "react": "^18", "react-dom": "^18" diff --git a/recommender/tailwind.config.ts b/recommender/tailwind.config.ts index 7e4bd91..386f35e 100644 --- a/recommender/tailwind.config.ts +++ b/recommender/tailwind.config.ts @@ -4,15 +4,11 @@ const config: Config = { content: [ "./pages/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}", + "./app/*.{js,ts,jsx,tsx,mdx}", "./app/**/*.{js,ts,jsx,tsx,mdx}", ], theme: { extend: { - backgroundImage: { - "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", - "gradient-conic": - "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", - }, }, }, plugins: [], diff --git a/recommender/tailwind.config.js b/recommender/unused-tailwind.config.js similarity index 87% rename from recommender/tailwind.config.js rename to recommender/unused-tailwind.config.js index 26d3851..564aa9b 100644 --- a/recommender/tailwind.config.js +++ b/recommender/unused-tailwind.config.js @@ -1,6 +1,7 @@ /** @type {import('tailwindcss').Config} */ module.exports = { content: [ + "./app/*.{js,ts,jsx,tsx,mdx}", "./app/**/*.{js,ts,jsx,tsx,mdx}", "./pages/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}" diff --git a/recommender/utils/locales.tsx b/recommender/utils/locales.tsx new file mode 100644 index 0000000..8972589 --- /dev/null +++ b/recommender/utils/locales.tsx @@ -0,0 +1,43 @@ +export const locales = [ + { code: "ar", language: "العربية", dir: "rtl" }, + { code: "be", language: "Беларуская" }, + { code: "bn", language: "বাংলা" }, + { code: "ca", language: "Català" }, + { code: "cs", language: "Čeština" }, + { code: "cy", language: "Cymraeg" }, + { code: "de", language: "Deutsch" }, + { code: "en", language: "English" }, + { code: "eo", language: "Esperanto" }, + { code: "es", language: "Español" }, + { code: "eu", language: "Euskara" }, + { code: "fa", language: "فارسی", dir: "rtl" }, + { code: "fi", language: "Suomi" }, + { code: "fr", language: "Français" }, + { code: "fy", language: "Frysk" }, + { code: "ga", language: "Gaeilge" }, + { code: "gd", language: "Gàidhlig" }, + { code: "gl", language: "Galego" }, + { code: "he", language: "עברית", dir: "rtl" }, + { code: "is", language: "Íslenska" }, + { code: "it", language: "Italiano" }, + { code: "ja", language: "日本語" }, + { code: "kab", language: "Taqbaylit" }, + { code: "ko", language: "한국어" }, + { code: "ku", language: "Kurmancî" }, + { code: "nl-NL", language: "Nederlands" }, + { code: "no", language: "Norsk" }, + { code: "pl", language: "Polski" }, + { code: "pt-BR", language: "Português" }, + { code: "ru", language: "Русский" }, + { code: "si", language: "සිංහල" }, + { code: "sk", language: "Slovenčina" }, + { code: "sl", language: "Slovenščina" }, + { code: "sq", language: "Shqip" }, + { code: "sv", language: "Svenska" }, + { code: "th", language: "ภาษาไทย" }, + { code: "tr", language: "Türkçe" }, + { code: "uk", language: "Українська" }, + { code: "vi", language: "Tiếng Việt" }, + { code: "zh", language: "中文" }, + { code: "zh-TW", language: "繁體中文" }, + ]; \ No newline at end of file diff --git a/recommender/utils/math.tsx b/recommender/utils/math.tsx new file mode 100644 index 0000000..cc235c3 --- /dev/null +++ b/recommender/utils/math.tsx @@ -0,0 +1,44 @@ +function dotProduct(a: number[], b: number[]): number { + return a.map((x, i) => x * b[i]).reduce((sum, current) => sum + current, 0); + } + + function magnitude(arr: number[]): number { + return Math.sqrt(arr.map(x => x * x).reduce((sum, current) => sum + current, 0)); + } + + function cosineSimilarity(arr1: number[], arr2: number[]): number { + if (arr1.length !== arr2.length) { + throw new Error("Arrays must have the same length"); + } + + const dotProd = dotProduct(arr1, arr2); + const magnitudeProd = magnitude(arr1) * magnitude(arr2); + if (magnitudeProd === 0) { + return 0; + } + return dotProd / magnitudeProd; + } + + function averageOfArrays(arr: number[][]): number[] { + // Get the length of the first sub-array + const length = arr[0].length; + + // Initialize an array to store the sums + const sums = Array(length).fill(0); + + // Loop over each sub-array + for (let i = 0; i < arr.length; i++) { + // Loop over each element in the sub-array + for (let j = 0; j < arr[i].length; j++) { + // Add the element to the corresponding sum + sums[j] += arr[i][j]; + } + } + + // Divide each sum by the number of arrays to get the average + const averages = sums.map(sum => sum / arr.length); + + return averages; + } + + export { cosineSimilarity, averageOfArrays }; \ No newline at end of file diff --git a/recommender/utils/other.tsx b/recommender/utils/other.tsx new file mode 100644 index 0000000..139597f --- /dev/null +++ b/recommender/utils/other.tsx @@ -0,0 +1,2 @@ + + diff --git a/recommender/utils/types.tsx b/recommender/utils/types.tsx new file mode 100644 index 0000000..889f1c0 --- /dev/null +++ b/recommender/utils/types.tsx @@ -0,0 +1,41 @@ +import { cosineSimilarity } from "@/utils/math"; + +interface Tag { + index: number; + tag: string; +} + +export function isSensitive(tag: Tag) { + return tag.tag.endsWith("_sensitive") +} + +export const selected_tags = [ + "politics", "gardening", "art", "nature", "pride", "cycling", "climate", + "programming", "dogs", "privacy", "cats", "gaming", "education", + "science", "music", "movies", "food", "books", "lgbtq", "python", + "emacs", "gay", "trans", "furry", "photography", "cooking", "literature", + "television" + ]; + + export function find_most_similar_tags(all_tags: Tag[], tag_names: string[], selectedTagIds: number[], positions: number[][]) { + let most_similar: Tag[] = []; + // get the average position of all selected tags + if (selectedTagIds.length > 0) { + // loop through all selected tags and get their positions + const selected_positions = selectedTagIds.map((tagId) => positions[tagId]); + for (let i = 0; i < selected_positions.length; i++) { + let tag_similarity = positions.map((row) => cosineSimilarity(selected_positions[i], row)); + let tag_rank = tag_similarity.map((similarity, index) => ({index, similarity})).sort((a, b) => b.similarity - a.similarity).map((item, index) => ({index: item.index, similarity: item.similarity, name: tag_names[item.index]})); + console.log(tag_rank.slice(0, 10)); + let topServerIds = tag_rank.slice(0, 10).map((item) => item.index); + for (let tagId of all_tags.filter((tag) => topServerIds.includes(tag.index))) { + if (!selectedTagIds.includes(tagId.index) && !most_similar.includes(tagId)) { + most_similar.push(tagId); + } + } + } + } + return most_similar; + } + +export type { Tag }; \ No newline at end of file