From 81f9803f39f58251bac3b89d6d4e3c9986b74d71 Mon Sep 17 00:00:00 2001 From: hotman78 Date: Wed, 14 May 2025 09:54:37 +0900 Subject: [PATCH 1/4] =?UTF-8?q?recommendation=20=E3=81=AB=E3=82=B3?= =?UTF-8?q?=E3=83=B3=E3=83=86=E3=82=B9=E3=83=88=E3=82=AB=E3=83=86=E3=82=B4?= =?UTF-8?q?=E3=83=AA=E3=81=AB=E3=82=88=E3=82=8B=E3=83=95=E3=82=A3=E3=83=AB?= =?UTF-8?q?=E3=82=BF=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Recommendations/RecommendController.tsx | 32 ++++++++++++++++++- .../Recommendations/RecommendProblems.ts | 4 ++- .../pages/UserPage/Recommendations/index.tsx | 26 +++++++++++++-- .../src/utils/LocalStorage.tsx | 1 + 4 files changed, 59 insertions(+), 4 deletions(-) diff --git a/atcoder-problems-frontend/src/pages/UserPage/Recommendations/RecommendController.tsx b/atcoder-problems-frontend/src/pages/UserPage/Recommendations/RecommendController.tsx index aa8410a03..d4b65a30d 100644 --- a/atcoder-problems-frontend/src/pages/UserPage/Recommendations/RecommendController.tsx +++ b/atcoder-problems-frontend/src/pages/UserPage/Recommendations/RecommendController.tsx @@ -9,7 +9,6 @@ import { UncontrolledDropdown, } from "reactstrap"; import { formatExcludeOption } from "../../../utils/LastSolvedTime"; - const RECOMMEND_NUM_OPTIONS = [ { text: "10", @@ -47,6 +46,15 @@ const ExcludeOptions = [ ] as const; export type ExcludeOption = typeof ExcludeOptions[number]; +const CategoryOptions = [ + "All", + "ABC", + "ARC", + "AGC", + "Other Sponsored", +] as const; +export type CategoryOption = typeof CategoryOptions[number]; + interface Props { recommendOption: RecommendOption; onChangeRecommendOption: (option: RecommendOption) => void; @@ -54,6 +62,9 @@ interface Props { excludeOption: ExcludeOption; onChangeExcludeOption: (option: ExcludeOption) => void; + categoryOption: CategoryOption; + onChangeCategoryOption: (option: CategoryOption) => void; + showExperimental: boolean; onChangeExperimentalVisibility: (showExperimental: boolean) => void; @@ -92,6 +103,25 @@ export const RecommendController = (props: Props) => ( + + + + {props.categoryOption === "All" + ? "Contest Category" + : props.categoryOption} + + + {CategoryOptions.map((option) => ( + props.onChangeCategoryOption(option)} + > + {option} + + ))} + + + boolean, + isIncludedContestCategory: (contestId: ContestId) => boolean, getProblemModel: (problemId: ProblemId) => ProblemModel | undefined, recommendExperimental: boolean, internalRating: number | null, @@ -68,6 +69,7 @@ export const recommendProblems = ( const recommendRange = getRecommendProbabilityRange(recommendOption); return problems .filter((p) => isIncluded(p.id)) + .filter((p) => isIncludedContestCategory(p.contest_id)) .map((p) => ({ ...p, difficulty: getProblemModel(p.id)?.difficulty, diff --git a/atcoder-problems-frontend/src/pages/UserPage/Recommendations/index.tsx b/atcoder-problems-frontend/src/pages/UserPage/Recommendations/index.tsx index 9c556870e..1d8aaecd4 100644 --- a/atcoder-problems-frontend/src/pages/UserPage/Recommendations/index.tsx +++ b/atcoder-problems-frontend/src/pages/UserPage/Recommendations/index.tsx @@ -15,7 +15,7 @@ import { HelpBadgeTooltip } from "../../../components/HelpBadgeTooltip"; import { NewTabLink } from "../../../components/NewTabLink"; import { ProblemLink } from "../../../components/ProblemLink"; import Problem from "../../../interfaces/Problem"; -import { ProblemId } from "../../../interfaces/Status"; +import { ContestId, ProblemId } from "../../../interfaces/Status"; import { formatPredictedSolveProbability, formatPredictedSolveTime, @@ -34,8 +34,13 @@ import { getLastSolvedTimeMap, getMaximumExcludeElapsedSecond, } from "../../../utils/LastSolvedTime"; +import { classifyContest } from "../../../utils/ContestClassifier"; import { recommendProblems } from "./RecommendProblems"; -import { RecommendController, RecommendOption } from "./RecommendController"; +import { + CategoryOption, + RecommendController, + RecommendOption, +} from "./RecommendController"; interface Props { userId: string; @@ -54,6 +59,10 @@ export const Recommendations = (props: Props) => { "recoomendExcludeOption", "Exclude" ); + const [categoryOption, setCategoryOption] = useLocalStorage( + "recommendCategoryOption", + "All" + ); const [recommendNum, setRecommendNum] = useState(10); const [ @@ -105,6 +114,17 @@ export const Recommendations = (props: Props) => { : Number.MAX_SAFE_INTEGER; return getMaximumExcludeElapsedSecond(excludeOption) < elapsedSecond; }, + (contestId: ContestId) => { + if (categoryOption === "All") return true; + const contest = contestMap?.get(contestId); + if (!contest) { + return false; + } + return ( + classifyContest(contest) === categoryOption || + classifyContest(contest) === categoryOption + "-Like" + ); + }, (problemId: ProblemId) => problemModels?.get(problemId), recommendExperimental, userRatingInfo.internalRating, @@ -120,6 +140,8 @@ export const Recommendations = (props: Props) => { onChangeRecommendOption={(option) => setRecommendOption(option)} excludeOption={excludeOption} onChangeExcludeOption={(option) => setExcludeOption(option)} + categoryOption={categoryOption} + onChangeCategoryOption={(option) => setCategoryOption(option)} showExperimental={recommendExperimental} onChangeExperimentalVisibility={(show) => setRecommendExperimental(show) diff --git a/atcoder-problems-frontend/src/utils/LocalStorage.tsx b/atcoder-problems-frontend/src/utils/LocalStorage.tsx index 7cfa14de2..c7bd007de 100644 --- a/atcoder-problems-frontend/src/utils/LocalStorage.tsx +++ b/atcoder-problems-frontend/src/utils/LocalStorage.tsx @@ -17,6 +17,7 @@ const LocalStorageKeys = [ "recommendOption", "recommendExperimental", "recoomendExcludeOption", + "recommendCategoryOption", "MergeLikeContest", ] as const; type LocalStorageKey = typeof LocalStorageKeys[number]; From 2d7ba57362253bf63ed5a8ad3a2d76f530f09bd9 Mon Sep 17 00:00:00 2001 From: hotman78 Date: Wed, 14 May 2025 10:41:51 +0900 Subject: [PATCH 2/4] =?UTF-8?q?ProblemList=20=E3=81=AB=E3=82=B3=E3=83=B3?= =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E3=82=AB=E3=83=86=E3=82=B4=E3=83=AA?= =?UTF-8?q?=E3=81=AB=E3=82=88=E3=82=8B=E3=83=95=E3=82=A3=E3=83=AB=E3=82=BF?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/ListPage/ListTable.tsx | 9 ++++++ .../src/pages/ListPage/ProblemList.tsx | 31 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/atcoder-problems-frontend/src/pages/ListPage/ListTable.tsx b/atcoder-problems-frontend/src/pages/ListPage/ListTable.tsx index 35dd95b49..aea3ceb21 100644 --- a/atcoder-problems-frontend/src/pages/ListPage/ListTable.tsx +++ b/atcoder-problems-frontend/src/pages/ListPage/ListTable.tsx @@ -42,6 +42,7 @@ import { useProblemModelMap, useRatingInfo, } from "../../api/APIClient"; +import { classifyContest } from "../../utils/ContestClassifier"; export const INF_POINT = 1e18; @@ -113,6 +114,7 @@ interface Props { | "Only Rated" | "Only Unrated" | "Only Unrated without Difficulty"; + contestCategoryFilterState: string; fromDifficulty: number; toDifficulty: number; filteredSubmissions: Submission[]; @@ -629,6 +631,13 @@ export const ListTable: React.FC = (props) => { return !isRated && !hasDifficulty; } }) + .filter((row): boolean => { + if (props.contestCategoryFilterState === "All") return true; + if (!row.contest) return false; + const contest = contestMap?.get(row.contest.id); + const contestCategory = classifyContest(contest); + return props.contestCategoryFilterState === contestCategory; + }) .filter((row) => { const difficulty = isProblemModelWithDifficultyModel(row.problemModel) ? row.problemModel.difficulty diff --git a/atcoder-problems-frontend/src/pages/ListPage/ProblemList.tsx b/atcoder-problems-frontend/src/pages/ListPage/ProblemList.tsx index 6d16e28e0..9e09b21a4 100644 --- a/atcoder-problems-frontend/src/pages/ListPage/ProblemList.tsx +++ b/atcoder-problems-frontend/src/pages/ListPage/ProblemList.tsx @@ -24,6 +24,7 @@ import { } from "../../utils/ProblemSelection"; import { useLoginState } from "../../api/InternalAPIClient"; import { useMergedProblemMap } from "../../api/APIClient"; +import { ContestCategories } from "../../utils/ContestClassifier"; import { INF_POINT, ListTable, StatusFilter, statusFilters } from "./ListTable"; export const FilterParams = { @@ -31,6 +32,7 @@ export const FilterParams = { ToPoint: "toPo", Status: "status", Rated: "rated", + Category: "category", FromDifficulty: "fromDiff", ToDifficulty: "toDiff", Language: "Lang", @@ -55,6 +57,8 @@ const RATED_FILTERS = [ ] as const; type RatedFilter = typeof RATED_FILTERS[number]; +const categoryFilters = ["All", ...ContestCategories] as const; +type CategoryFilter = typeof categoryFilters[number]; interface Props { userId: string; submissions: Submission[]; @@ -91,6 +95,10 @@ export const ProblemList: React.FC = (props) => { const ratedFilterState: RatedFilter = RATED_FILTERS.find((x) => x === searchParams.get(FilterParams.Rated)) ?? "All"; + const contestCategoryFilterState: CategoryFilter = + categoryFilters.find( + (x) => x === searchParams.get(FilterParams.Category) + ) ?? "All"; const languages = ["All"].concat( Array.from( @@ -210,6 +218,28 @@ export const ProblemList: React.FC = (props) => { + + + + {contestCategoryFilterState === "All" + ? "Category" + : contestCategoryFilterState} + + + {categoryFilters.map((value) => ( + + {value} + + ))} + + + @@ -325,6 +355,7 @@ export const ProblemList: React.FC = (props) => { toPoint={toPoint} statusFilterState={statusFilterState} ratedFilterState={ratedFilterState} + contestCategoryFilterState={contestCategoryFilterState} fromDifficulty={fromDifficulty} toDifficulty={toDifficulty} filteredSubmissions={props.submissions} From 4381c40ca5c3521cb3faa453e048b44a888a537f Mon Sep 17 00:00:00 2001 From: hotman78 Date: Wed, 14 May 2025 15:46:39 +0900 Subject: [PATCH 3/4] =?UTF-8?q?recommend=20=E3=81=AE=E3=82=AB=E3=83=86?= =?UTF-8?q?=E3=82=B4=E3=83=AA=E5=88=86=E3=81=91=E3=81=AE=20merge=20-Like?= =?UTF-8?q?=20=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Recommendations/RecommendController.tsx | 198 ++++++++++-------- .../pages/UserPage/Recommendations/index.tsx | 12 +- .../src/utils/LocalStorage.tsx | 1 + 3 files changed, 127 insertions(+), 84 deletions(-) diff --git a/atcoder-problems-frontend/src/pages/UserPage/Recommendations/RecommendController.tsx b/atcoder-problems-frontend/src/pages/UserPage/Recommendations/RecommendController.tsx index d4b65a30d..9f4639fa3 100644 --- a/atcoder-problems-frontend/src/pages/UserPage/Recommendations/RecommendController.tsx +++ b/atcoder-problems-frontend/src/pages/UserPage/Recommendations/RecommendController.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import { React, useMemo } from "react"; import { Button, ButtonGroup, @@ -9,6 +9,7 @@ import { UncontrolledDropdown, } from "reactstrap"; import { formatExcludeOption } from "../../../utils/LastSolvedTime"; +import { isLikeContestCategory } from "../../../utils/LikeContestUtils"; const RECOMMEND_NUM_OPTIONS = [ { text: "10", @@ -51,6 +52,9 @@ const CategoryOptions = [ "ABC", "ARC", "AGC", + "ABC-Like", + "ARC-Like", + "AGC-Like", "Other Sponsored", ] as const; export type CategoryOption = typeof CategoryOptions[number]; @@ -67,90 +71,118 @@ interface Props { showExperimental: boolean; onChangeExperimentalVisibility: (showExperimental: boolean) => void; + mergeLikeContest: boolean; + onChangeMergeLikeContest: (mergeLikeContest: boolean) => void; showCount: number; onChangeShowCount: (count: number) => void; } -export const RecommendController = (props: Props) => ( - <> -
- - {RecommendOptions.map((type) => ( - - ))} - - - - - {formatExcludeOption(props.excludeOption)} - - - {ExcludeOptions.map((option) => ( - props.onChangeExcludeOption(option)} - > - {formatExcludeOption(option)} - - ))} - - - - - - - {props.categoryOption === "All" - ? "Contest Category" - : props.categoryOption} - - - {CategoryOptions.map((option) => ( - props.onChangeCategoryOption(option)} - > - {option} - - ))} - - - - - 🧪 - - } - checked={props.showExperimental} - onChange={() => - props.onChangeExperimentalVisibility(!props.showExperimental) - } - /> -
- - - {props.showCount === Number.POSITIVE_INFINITY ? "All" : props.showCount} - - - {RECOMMEND_NUM_OPTIONS.map(({ text, value }) => ( - props.onChangeShowCount(value)} - > - {text} - - ))} - - - -); +export const RecommendController = (props: Props) => { + const filteredCategories = useMemo(() => { + return CategoryOptions.filter( + (category) => + category === "All" || + !props.mergeLikeContest || + !isLikeContestCategory(category) + ); + }, [props.mergeLikeContest]); + return ( + <> +
+ + {RecommendOptions.map((type) => ( + + ))} + + + + + {formatExcludeOption(props.excludeOption)} + + + {ExcludeOptions.map((option) => ( + props.onChangeExcludeOption(option)} + > + {formatExcludeOption(option)} + + ))} + + + + + + + {props.categoryOption === "All" + ? "Contest Category" + : props.categoryOption} + + + {filteredCategories.map((option) => ( + props.onChangeCategoryOption(option)} + > + {option} + + ))} + + + + + 🧪 + + } + checked={props.showExperimental} + onChange={() => + props.onChangeExperimentalVisibility(!props.showExperimental) + } + /> + + Merge "-Like" Contests + + } + checked={props.mergeLikeContest} + onChange={() => + props.onChangeMergeLikeContest(!props.mergeLikeContest) + } + /> +
+ + + {props.showCount === Number.POSITIVE_INFINITY + ? "All" + : props.showCount} + + + {RECOMMEND_NUM_OPTIONS.map(({ text, value }) => ( + props.onChangeShowCount(value)} + > + {text} + + ))} + + + + ); +}; diff --git a/atcoder-problems-frontend/src/pages/UserPage/Recommendations/index.tsx b/atcoder-problems-frontend/src/pages/UserPage/Recommendations/index.tsx index 1d8aaecd4..7c75c4b64 100644 --- a/atcoder-problems-frontend/src/pages/UserPage/Recommendations/index.tsx +++ b/atcoder-problems-frontend/src/pages/UserPage/Recommendations/index.tsx @@ -35,6 +35,7 @@ import { getMaximumExcludeElapsedSecond, } from "../../../utils/LastSolvedTime"; import { classifyContest } from "../../../utils/ContestClassifier"; +import { getLikeContestCategory } from "../../../utils/LikeContestUtils"; import { recommendProblems } from "./RecommendProblems"; import { CategoryOption, @@ -59,6 +60,12 @@ export const Recommendations = (props: Props) => { "recoomendExcludeOption", "Exclude" ); + + const [mergeLikeContest, setMergeLikeContest] = useLocalStorage( + "recommendMergeLikeContest", + true + ); + const [categoryOption, setCategoryOption] = useLocalStorage( "recommendCategoryOption", "All" @@ -122,7 +129,8 @@ export const Recommendations = (props: Props) => { } return ( classifyContest(contest) === categoryOption || - classifyContest(contest) === categoryOption + "-Like" + (mergeLikeContest && + classifyContest(contest) === getLikeContestCategory(categoryOption)) ); }, (problemId: ProblemId) => problemModels?.get(problemId), @@ -146,6 +154,8 @@ export const Recommendations = (props: Props) => { onChangeExperimentalVisibility={(show) => setRecommendExperimental(show) } + mergeLikeContest={mergeLikeContest} + onChangeMergeLikeContest={(merge) => setMergeLikeContest(merge)} showCount={recommendNum} onChangeShowCount={(value) => setRecommendNum(value)} /> diff --git a/atcoder-problems-frontend/src/utils/LocalStorage.tsx b/atcoder-problems-frontend/src/utils/LocalStorage.tsx index c7bd007de..f78e14923 100644 --- a/atcoder-problems-frontend/src/utils/LocalStorage.tsx +++ b/atcoder-problems-frontend/src/utils/LocalStorage.tsx @@ -16,6 +16,7 @@ const LocalStorageKeys = [ "showRating", "recommendOption", "recommendExperimental", + "recommendMergeLikeContest", "recoomendExcludeOption", "recommendCategoryOption", "MergeLikeContest", From f11e90970140ea569816a1c2b9f672eff4a3eaf7 Mon Sep 17 00:00:00 2001 From: hotman78 Date: Wed, 14 May 2025 16:40:57 +0900 Subject: [PATCH 4/4] =?UTF-8?q?List=20=E3=81=AE=E3=82=AB=E3=83=86=E3=82=B4?= =?UTF-8?q?=E3=83=AA=E5=88=86=E3=81=91=E3=81=AE=20merge=20-Like=20?= =?UTF-8?q?=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/ListPage/ListTable.tsx | 9 +++++++- .../src/pages/ListPage/ProblemList.tsx | 23 ++++++++++++++++--- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/atcoder-problems-frontend/src/pages/ListPage/ListTable.tsx b/atcoder-problems-frontend/src/pages/ListPage/ListTable.tsx index aea3ceb21..031fa5bce 100644 --- a/atcoder-problems-frontend/src/pages/ListPage/ListTable.tsx +++ b/atcoder-problems-frontend/src/pages/ListPage/ListTable.tsx @@ -43,6 +43,7 @@ import { useRatingInfo, } from "../../api/APIClient"; import { classifyContest } from "../../utils/ContestClassifier"; +import { getLikeContestCategory } from "../../utils/LikeContestUtils"; export const INF_POINT = 1e18; @@ -115,6 +116,7 @@ interface Props { | "Only Unrated" | "Only Unrated without Difficulty"; contestCategoryFilterState: string; + mergeLikeContest: boolean; fromDifficulty: number; toDifficulty: number; filteredSubmissions: Submission[]; @@ -636,7 +638,12 @@ export const ListTable: React.FC = (props) => { if (!row.contest) return false; const contest = contestMap?.get(row.contest.id); const contestCategory = classifyContest(contest); - return props.contestCategoryFilterState === contestCategory; + return ( + props.contestCategoryFilterState === contestCategory || + (props.mergeLikeContest && + getLikeContestCategory(props.contestCategoryFilterState) === + contestCategory) + ); }) .filter((row) => { const difficulty = isProblemModelWithDifficultyModel(row.problemModel) diff --git a/atcoder-problems-frontend/src/pages/ListPage/ProblemList.tsx b/atcoder-problems-frontend/src/pages/ListPage/ProblemList.tsx index 9e09b21a4..8c9e3b407 100644 --- a/atcoder-problems-frontend/src/pages/ListPage/ProblemList.tsx +++ b/atcoder-problems-frontend/src/pages/ListPage/ProblemList.tsx @@ -1,5 +1,5 @@ import { Range } from "immutable"; -import React from "react"; +import { React, useMemo, useState } from "react"; import { Link, useHistory, useLocation } from "react-router-dom"; import { Button, @@ -25,6 +25,7 @@ import { import { useLoginState } from "../../api/InternalAPIClient"; import { useMergedProblemMap } from "../../api/APIClient"; import { ContestCategories } from "../../utils/ContestClassifier"; +import { isLikeContestCategory } from "../../utils/LikeContestUtils"; import { INF_POINT, ListTable, StatusFilter, statusFilters } from "./ListTable"; export const FilterParams = { @@ -35,6 +36,7 @@ export const FilterParams = { Category: "category", FromDifficulty: "fromDiff", ToDifficulty: "toDiff", + mergeLikeContest: "mergeLikeContest", Language: "Lang", } as const; @@ -64,7 +66,7 @@ interface Props { submissions: Submission[]; } -export const ProblemList: React.FC = (props) => { +export const ProblemList: React.FC = (props: Props) => { const location = useLocation(); const history = useHistory(); @@ -120,6 +122,7 @@ export const ProblemList: React.FC = (props) => { searchParams.get(FilterParams.ToDifficulty) || INF_POINT.toString(), 10 ); + const [mergeLikeContest, setMergeLikeContest] = useState(true); const mergedProblemMap = useMergedProblemMap().data ?? new Map(); const points = Array.from( @@ -138,6 +141,14 @@ export const ProblemList: React.FC = (props) => { const loginState = useLoginState().data; const isLoggedIn = UserState.isLoggedIn(loginState); + const filteredCategoryFilters = useMemo(() => { + return categoryFilters.filter( + (category) => + category === "All" || + !mergeLikeContest || + !isLikeContestCategory(category) + ); + }, [mergeLikeContest]); return ( <> @@ -226,7 +237,7 @@ export const ProblemList: React.FC = (props) => { : contestCategoryFilterState}
- {categoryFilters.map((value) => ( + {filteredCategoryFilters.map((value) => ( = (props) => { ))}
+
@@ -356,6 +372,7 @@ export const ProblemList: React.FC = (props) => { statusFilterState={statusFilterState} ratedFilterState={ratedFilterState} contestCategoryFilterState={contestCategoryFilterState} + mergeLikeContest={mergeLikeContest} fromDifficulty={fromDifficulty} toDifficulty={toDifficulty} filteredSubmissions={props.submissions}