From 91c79bf4131bb6240bcee3a0ea4a9ab80d9b8b28 Mon Sep 17 00:00:00 2001 From: Hoshino Date: Tue, 11 Aug 2026 11:37:40 +0800 Subject: [PATCH 1/5] refactor(webview): refactor webview --- packages/webview/src/ContestApp.tsx | 525 ++++---------- packages/webview/src/SidebarApp.tsx | 159 ++--- packages/webview/src/SubmissionDetailApp.tsx | 76 +- packages/webview/src/WebviewApp.tsx | 647 +++++------------- .../src/components/CookieSettingsPanel.tsx | 96 +++ .../webview/src/components/ProblemView.tsx | 103 +++ packages/webview/src/components/SampleBox.tsx | 73 ++ .../webview/src/components/StatusBadge.tsx | 15 + .../src/components/SubmissionHistory.tsx | 87 +++ .../webview/src/components/SubmitPanel.tsx | 148 ++++ packages/webview/src/components/TaskList.tsx | 43 ++ packages/webview/src/hooks/useTranslation.ts | 64 ++ .../webview/src/hooks/useWebviewMessage.ts | 19 + packages/webview/src/utils/format.ts | 8 + packages/webview/src/utils/status.ts | 21 + 15 files changed, 1049 insertions(+), 1035 deletions(-) create mode 100644 packages/webview/src/components/CookieSettingsPanel.tsx create mode 100644 packages/webview/src/components/ProblemView.tsx create mode 100644 packages/webview/src/components/SampleBox.tsx create mode 100644 packages/webview/src/components/StatusBadge.tsx create mode 100644 packages/webview/src/components/SubmissionHistory.tsx create mode 100644 packages/webview/src/components/SubmitPanel.tsx create mode 100644 packages/webview/src/components/TaskList.tsx create mode 100644 packages/webview/src/hooks/useTranslation.ts create mode 100644 packages/webview/src/hooks/useWebviewMessage.ts create mode 100644 packages/webview/src/utils/format.ts create mode 100644 packages/webview/src/utils/status.ts diff --git a/packages/webview/src/ContestApp.tsx b/packages/webview/src/ContestApp.tsx index 294891b..418259a 100644 --- a/packages/webview/src/ContestApp.tsx +++ b/packages/webview/src/ContestApp.tsx @@ -2,8 +2,14 @@ import React from "react"; import { Button, Card, Spinner } from "@template/ui"; import { useVSCode } from "./VSCodeProvider"; import { useI18n } from "./i18n"; -import type { ContestProblem, SampleCase, Standing, SubmissionRecord, WebviewMessage } from "./types"; -import { HtmlContent, TranslatedBlock } from "./components/HtmlContent"; +import { ProblemView } from "./components/ProblemView"; +import { SubmitPanel } from "./components/SubmitPanel"; +import { SubmissionList } from "./components/SubmissionHistory"; +import { TaskList } from "./components/TaskList"; +import { HtmlContent } from "./components/HtmlContent"; +import { useWebviewMessage } from "./hooks/useWebviewMessage"; +import { useTranslation } from "./hooks/useTranslation"; +import type { ContestProblem, Standing, SubmissionRecord } from "./types"; type Tab = "info" | "task" | "submit" | "rating"; @@ -35,30 +41,6 @@ const TABS: Array<{ key: Tab; label: string }> = [ { key: "rating", label: "ui.tabRating" }, ]; -const statusColor = (status: string): string => { - switch (status) { - case "AC": - return "text-green-500 bg-green-500/10"; - case "WA": - return "text-red-500 bg-red-500/10"; - case "TLE": - return "text-cyan-500 bg-cyan-500/10"; - case "MLE": - return "text-yellow-500 bg-yellow-500/10"; - case "RE": - return "text-purple-500 bg-purple-500/10"; - case "CE": - return "text-gray-400 bg-gray-400/10"; - default: - return "text-gray-400 bg-gray-400/10"; - } -}; - -const formatSubmitTime = (time: string): string => { - const m = time.match(/(\d{2})-(\d{2}) (\d{2}:\d{2})/); - return m ? `${m[1]}-${m[2]} ${m[3]}` : time; -}; - const ContestApp: React.FC = ({ initContest = "" }) => { const vscode = useVSCode(); const { t } = useI18n(); @@ -90,22 +72,18 @@ const ContestApp: React.FC = ({ initContest = "" }) => { const [loadingStandings, setLoadingStandings] = React.useState(false); const standingsCache = React.useRef>({}); - const [translatedCache, setTranslatedCache] = React.useState>>({}); - const [translated, setTranslated] = React.useState | null>(null); - const [translating, setTranslating] = React.useState(false); - const [translationMode, setTranslationMode] = React.useState<"api" | "free">("free"); - const [isLoading, setIsLoading] = React.useState(false); const [status, setStatus] = React.useState(""); const [submissions, setSubmissions] = React.useState([]); const [loadingHistory, setLoadingHistory] = React.useState(false); const selectedTaskRef = React.useRef(selectedTask); selectedTaskRef.current = selectedTask; + const translation = useTranslation(selectedTaskRef, setStatus); const loadProblem = (task: string) => { setIsLoading(true); setStatus(t("status.fetchingProblem", { contest, task })); - setTranslated(translatedCache[task] ?? null); + translation.setTranslatedForTask(task); vscode.postMessage({ command: "loadProblem", contest, task }); }; @@ -116,15 +94,7 @@ const ContestApp: React.FC = ({ initContest = "" }) => { }; const doTranslate = () => { - if (!problem) return; - setTranslating(true); - setStatus(t("status.translating")); - const texts: Record = {}; - if (problem.statement) texts[t("text.problemStatement")] = problem.statement; - if (problem.constraints) texts[t("text.constraints")] = problem.constraints; - if (problem.inputFormat) texts[t("text.inputFormat")] = problem.inputFormat; - if (problem.outputFormat) texts[t("text.outputFormat")] = problem.outputFormat; - vscode.postMessage({ command: "translate", payload: texts, targetLang: "ZH", translationMode }); + translation.translate(problem); }; const doCopyMarkdown = () => { @@ -177,10 +147,6 @@ const ContestApp: React.FC = ({ initContest = "" }) => { } }; - const copySampleText = (key: string, text: string) => { - navigator.clipboard.writeText(text); - }; - React.useEffect(() => { setIsLoading(true); setStatus(t("status.loadingContest", { contest })); @@ -188,105 +154,93 @@ const ContestApp: React.FC = ({ initContest = "" }) => { vscode.postMessage({ command: "getCookie" }); }, []); - React.useEffect(() => { - const handleMessage = (event: MessageEvent) => { - const message = event.data as WebviewMessage; - if (message.type === "tasks") { - setTasks(message.tasks ?? []); - setSelectedTask(""); - setProblem(null); - setStatus(t("status.tasksLoaded", { count: (message.tasks ?? []).length })); - setIsLoading(false); - } - if (message.type === "contestInfo") { - setRated(message.Rated ?? false); - setContestTitle(message.title ?? ""); - setAnnouncement(message.announcement ?? ""); - } - if (message.type === "cf_challenge") { - setCfUrl(message.url ?? null); - setIsLoading(false); - setStatus(t("status.cfChallenge")); - } - if (message.type === "loginRequired") { - setIsLoading(false); - setStatus(t("status.loginRequiredSidebar")); - } - if (message.type === "problem") { - setProblem(message.problem ?? null); - setStatus(t("status.problemLoaded", { title: message.problem?.title ?? "" })); - setIsLoading(false); - } - if (message.type === "loading" || message.type === "update") { - setStatus(message.text ?? t("status.loading")); - } - if (message.type === "error") { - setStatus(message.text ?? t("err.operationFailed")); - setIsLoading(false); - setTranslating(false); - setLoadingStandings(false); - setLoadingHistory(false); - } - if (message.type === "cookieStatus") { - setHasCookie(message.hasCookie ?? false); - if (message.statusMessage) setStatus(message.statusMessage); - } - if (message.type === "registrationStatus") { - setSigned(message.signed ?? false); - setRegistrationMessage(message.registrationMessage ?? null); - setStatus(message.registrationMessage ?? (message.signed ? t("status.registrationSuccess") : t("status.registrationFail"))); - setIsLoading(false); - } - if (message.type === "translation") { - const key = selectedTaskRef.current; - setTranslatedCache((prev) => ({ ...prev, [key]: message.translated ?? {} })); - setTranslated(message.translated ?? null); - setTranslating(false); - setStatus(t("status.translationDone")); - } - if (message.type === "submitPage") { - setSubmitPageError(null); - setSubmitTasks(message.submitTasks ?? []); - setSubmitLanguages(message.languages ?? []); - if (message.submitTasks && message.submitTasks.length > 0) { - setSelectedSubmitTask(message.submitTasks[0].value); - } - if (message.languages && message.languages.length > 0) { - setSelectedSubmitLanguage(message.languages[0].id); - } - setStatus(t("status.submitPageReady")); - setIsLoading(false); - } - if (message.type === "submitPageError") { - setSubmitPageError({ message: message.message ?? t("err.submitPageUnavailable"), url: message.url }); - setStatus(message.message ?? t("err.submitPageUnavailable")); - setIsLoading(false); - } - if (message.type === "submitResult") { - setSubmitResult(message.submitResult ?? null); - setIsLoading(false); - setStatus(message.submitResult?.success ? t("status.submitSuccess") : (message.submitResult?.message ?? t("status.submitFailed"))); - } - if (message.type === "statusUpdate") { - const statuses = message.statuses ?? {}; - setTasks((prev) => prev.map((task) => ({ ...task, status: statuses[task.value] }))); + useWebviewMessage({ + tasks: (message) => { + setTasks(message.tasks ?? []); + setSelectedTask(""); + setProblem(null); + setStatus(t("status.tasksLoaded", { count: (message.tasks ?? []).length })); + setIsLoading(false); + }, + contestInfo: (message) => { + setRated(message.Rated ?? false); + setContestTitle(message.title ?? ""); + setAnnouncement(message.announcement ?? ""); + }, + cf_challenge: (message) => { + setCfUrl(message.url ?? null); + setIsLoading(false); + setStatus(t("status.cfChallenge")); + }, + loginRequired: () => { + setIsLoading(false); + setStatus(t("status.loginRequiredSidebar")); + }, + problem: (message) => { + setProblem(message.problem ?? null); + setStatus(t("status.problemLoaded", { title: message.problem?.title ?? "" })); + setIsLoading(false); + }, + loading: (message) => setStatus(message.text ?? t("status.loading")), + update: (message) => setStatus(message.text ?? t("status.loading")), + error: (message) => { + setStatus(message.text ?? t("err.operationFailed")); + setIsLoading(false); + translation.setTranslating(false); + setLoadingStandings(false); + setLoadingHistory(false); + }, + cookieStatus: (message) => { + setHasCookie(message.hasCookie ?? false); + if (message.statusMessage) setStatus(message.statusMessage); + }, + registrationStatus: (message) => { + setSigned(message.signed ?? false); + setRegistrationMessage(message.registrationMessage ?? null); + setStatus(message.registrationMessage ?? (message.signed ? t("status.registrationSuccess") : t("status.registrationFail"))); + setIsLoading(false); + }, + translation: (message) => translation.applyTranslation(message), + submitPage: (message) => { + setSubmitPageError(null); + setSubmitTasks(message.submitTasks ?? []); + setSubmitLanguages(message.languages ?? []); + if (message.submitTasks && message.submitTasks.length > 0) { + setSelectedSubmitTask(message.submitTasks[0].value); } - if (message.type === "submissionHistory") { - setSubmissions(message.submissions ?? []); - setLoadingHistory(false); - setStatus(t("status.historyLoaded", { count: (message.submissions ?? []).length })); + if (message.languages && message.languages.length > 0) { + setSelectedSubmitLanguage(message.languages[0].id); } - if (message.type === "standings") { - const list = message.standings ?? []; - standingsCache.current[contest] = list; - setStandings(list); - setLoadingStandings(false); - setStatus(t("status.standingsLoaded", { count: list.length })); - } - }; - window.addEventListener("message", handleMessage); - return () => window.removeEventListener("message", handleMessage); - }, []); + setStatus(t("status.submitPageReady")); + setIsLoading(false); + }, + submitPageError: (message) => { + setSubmitPageError({ message: message.message ?? t("err.submitPageUnavailable"), url: message.url }); + setStatus(message.message ?? t("err.submitPageUnavailable")); + setIsLoading(false); + }, + submitResult: (message) => { + setSubmitResult(message.submitResult ?? null); + setIsLoading(false); + setStatus(message.submitResult?.success ? t("status.submitSuccess") : (message.submitResult?.message ?? t("status.submitFailed"))); + }, + statusUpdate: (message) => { + const statuses = message.statuses ?? {}; + setTasks((prev) => prev.map((task) => ({ ...task, status: statuses[task.value] }))); + }, + submissionHistory: (message) => { + setSubmissions(message.submissions ?? []); + setLoadingHistory(false); + setStatus(t("status.historyLoaded", { count: (message.submissions ?? []).length })); + }, + standings: (message) => { + const list = message.standings ?? []; + standingsCache.current[contest] = list; + setStandings(list); + setLoadingStandings(false); + setStatus(t("status.standingsLoaded", { count: list.length })); + }, + }); const renderInfoTab = () => (
@@ -338,150 +292,27 @@ const ContestApp: React.FC = ({ initContest = "" }) => { const renderTaskTab = () => (
{tasks.length > 0 && ( - -
{t("ui.taskList")}
-
- {tasks.map((task) => ( - - ))} -
-
+ { + setSelectedTask(value); + loadProblem(value); + }} + /> )} {problem && ( - -
-
-
{problem.title}
-
{problem.url}
-
-
- - - - -
-
- - {problem.statement && ( -
-
{t("ui.statement")}
- - {translated?.[t("text.problemStatement")] && ( - - )} -
- )} - - {problem.constraints && ( -
-
{t("text.constraints")}
- - {translated?.[t("text.constraints")] && ( - - )} -
- )} - - {problem.inputFormat && ( -
-
{t("text.inputFormat")}
- - {translated?.[t("text.inputFormat")] && ( - - )} -
- )} - - {problem.outputFormat && ( -
-
{t("text.outputFormat")}
- - {translated?.[t("text.outputFormat")] && ( - - )} -
- )} - - {problem.samples?.length > 0 ? ( - problem.samples.map((sample: SampleCase) => ( -
-
{t("ui.sampleLabel", { index: sample.index })}
-
-
Input
-
{sample.input}
- -
-
-
Output
-
{sample.output}
- -
-
- )) - ) : problem.sampleUrl ? ( -
-
{t("ui.sampleExternal")}
- -
- ) : ( -
{t("ui.noSamples")}
- )} -
+ )} {!isLoading && tasks.length === 0 && ( @@ -520,86 +351,22 @@ const ContestApp: React.FC = ({ initContest = "" }) => {
- {submitPageError ? ( -
-
{submitPageError.message}
- -
- ) : submitTasks.length === 0 && submitLanguages.length === 0 ? ( -
{t("ui.clickToStart")}
- ) : ( -
-
-
{t("ui.task")}
- -
- -
-
{t("ui.language")}
- -
- -
-
{t("ui.sourceCode")}
-