From 6f4a42a22faa8b2ad05d4cb0fb014c8c172113eb Mon Sep 17 00:00:00 2001 From: THOTH Date: Thu, 12 Nov 2026 17:18:04 +0900 Subject: [PATCH] Fix defects found in the final QA pass and drop repeated headings The evidence panel of an earlier, folded round always said the evidence could not be read: that card shows the full stored result, while the check compared it only with the summary view. A shown result now passes when it is part of the summary view, the returned result or the manifest result (a mix of forms is still refused). An earlier round says its answer checks are shown only on the latest round instead of "not read yet". The English "Open dropdown" tooltip and span ids in citation tooltips are gone, the sidebar label no longer clips in English and Japanese, and two Korean particles after names are fixed. The home screen starts with the project list instead of a large slogan, eight small headings that repeated the title and one fragment label are removed. --- SOURCE_MANIFEST.json | 255 +++++++++--------- apps/web/src/api/resultEvidence.ts | 6 +- .../web/src/api/resultEvidenceRuntime.test.ts | 20 ++ apps/web/src/api/uploadTypes.ts | 1 - apps/web/src/components/ActionCompare.tsx | 2 +- apps/web/src/components/ActionEffort.tsx | 5 +- apps/web/src/components/AnswerBody.tsx | 2 +- .../src/components/AnswerCitations.test.tsx | 4 + apps/web/src/components/AnswerHold.tsx | 8 +- apps/web/src/components/Brand.tsx | 14 +- .../src/components/CapabilityWorkbench.tsx | 10 +- apps/web/src/components/ConfirmedFacts.tsx | 2 +- .../src/components/ConnectedSourceCard.tsx | 1 - .../ConversationEarlierRound.test.tsx | 76 ++++++ .../src/components/ConversationTimeline.tsx | 7 +- apps/web/src/components/DomainRecords.tsx | 3 +- apps/web/src/components/EvidenceRail.tsx | 9 +- .../src/components/FileConnectionPanel.tsx | 10 +- .../src/components/LiveProjectWorkspace.tsx | 7 +- .../LiveProjectWorkspaceRestart.test.tsx | 14 + .../web/src/components/ModelSettings.test.tsx | 5 +- apps/web/src/components/ModelSettings.tsx | 11 +- .../src/components/NavigationDrawer.test.tsx | 1 - apps/web/src/components/ProjectForm.test.tsx | 1 - apps/web/src/components/ProjectForm.tsx | 1 - .../src/components/ProjectSessionPages.tsx | 4 +- .../web/src/components/ResearchResultCard.tsx | 6 +- .../web/src/components/ResourcesPage.test.tsx | 1 - .../src/components/ResultCardOnce.test.tsx | 8 + apps/web/src/components/ResultHoldDetails.tsx | 6 +- apps/web/src/components/Select.test.tsx | 27 ++ apps/web/src/components/Select.tsx | 11 + .../web/src/components/SourceTimeAdvanced.tsx | 5 +- apps/web/src/components/WorkspaceTopbar.tsx | 1 - .../history/ResearchHistoryWorkspace.tsx | 3 +- apps/web/src/components/statusLabels.test.tsx | 2 +- .../web/src/components/useNavigationDrawer.ts | 1 - apps/web/src/i18n/apiNoticesMessages.ts | 4 + apps/web/src/i18n/countFormatMessages.ts | 2 +- apps/web/src/i18n/formatMessages.ts | 2 +- apps/web/src/i18n/ja/apiNoticesMessagesJa.ts | 1 + apps/web/src/i18n/ja/sourceMessagesJa.ts | 1 - .../src/i18n/ja/workspaceDetailMessagesJa.ts | 1 - apps/web/src/i18n/ja/workspaceMessagesJa.ts | 2 - apps/web/src/i18n/sourceMessages.ts | 4 - apps/web/src/i18n/statusChinese.ts | 4 +- apps/web/src/i18n/statusEnglish.ts | 4 +- apps/web/src/i18n/statusJapanese.ts | 4 +- apps/web/src/i18n/statusResources.ts | 4 +- apps/web/src/i18n/statusTraditional.ts | 4 +- apps/web/src/i18n/wordingRules.test.ts | 40 +++ apps/web/src/i18n/workspaceDetailMessages.ts | 4 - apps/web/src/i18n/workspaceMessages.ts | 8 - apps/web/src/i18n/zh/apiNoticesMessagesZh.ts | 1 + apps/web/src/i18n/zh/sourceMessagesZh.ts | 1 - .../src/i18n/zh/workspaceDetailMessagesZh.ts | 1 - apps/web/src/i18n/zh/workspaceMessagesZh.ts | 2 - .../i18n/zhHant/apiNoticesMessagesZhHant.ts | 1 + .../src/i18n/zhHant/sourceMessagesZhHant.ts | 1 - .../zhHant/workspaceDetailMessagesZhHant.ts | 1 - .../i18n/zhHant/workspaceMessagesZhHant.ts | 2 - apps/web/src/narrowLayoutRules.test.ts | 10 + apps/web/src/typeScaleRules.test.ts | 1 - apps/web/src/workstation.css | 10 +- 64 files changed, 439 insertions(+), 231 deletions(-) create mode 100644 apps/web/src/components/ConversationEarlierRound.test.tsx create mode 100644 apps/web/src/components/Select.test.tsx create mode 100644 apps/web/src/components/Select.tsx diff --git a/SOURCE_MANIFEST.json b/SOURCE_MANIFEST.json index 8cac7638..7fd01ab2 100644 --- a/SOURCE_MANIFEST.json +++ b/SOURCE_MANIFEST.json @@ -1,6 +1,6 @@ { "schema_version": "thoth-source-preview-manifest-v1", - "created_at": "2026-11-12T05:53:40.032733+00:00", + "created_at": "2026-11-12T07:21:29.752175+00:00", "license": "MIT", "git_history_included": false, "full_suite_verified": false, @@ -478,8 +478,8 @@ }, { "path": "apps/web/src/api/resultEvidence.ts", - "sha256": "f1155987d8b7b4151754a72daef1d541194ae974ec491087f8ab99f93b153e47", - "bytes": 9588 + "sha256": "9a8911d113e4061226b443bd19dcb1911dc41141dbb115f8be7985b7d4e3f249", + "bytes": 9992 }, { "path": "apps/web/src/api/resultEvidenceIdentity.test.ts", @@ -493,8 +493,8 @@ }, { "path": "apps/web/src/api/resultEvidenceRuntime.test.ts", - "sha256": "d7f95bf35c84e6ca15d40a1828aef2ec9b552a487d0f3dc033a756167310abac", - "bytes": 4282 + "sha256": "0ab224cd47a485f159a5fedac254c9991cfe6cc69f9504ac02f8c9c9030b07de", + "bytes": 5818 }, { "path": "apps/web/src/api/resultEvidenceServerContract.test.ts", @@ -568,8 +568,8 @@ }, { "path": "apps/web/src/api/uploadTypes.ts", - "sha256": "55077e62dc103db1732ba91216b61990cd267e03837639fac0042e370509971d", - "bytes": 2481 + "sha256": "d0cb6292986348f493ad07aa3ad7da1cac8b9802c25ee505eff5138d960a11f2", + "bytes": 2480 }, { "path": "apps/web/src/components/AbsenceChecks.test.tsx", @@ -588,8 +588,8 @@ }, { "path": "apps/web/src/components/ActionCompare.tsx", - "sha256": "0955d2f6d0f5ab9b6f3bc7654a0c013658d5dd79542579e349532ff276bae75b", - "bytes": 7925 + "sha256": "1a3100723911d7996b8978cfe588e8002d00c7e1465896e7e300798d4f637dca", + "bytes": 7913 }, { "path": "apps/web/src/components/ActionCompareApproval.test.tsx", @@ -613,23 +613,23 @@ }, { "path": "apps/web/src/components/ActionEffort.tsx", - "sha256": "92c8bc80b892b2c2942df09a524cec8dfd1b96460289717f94e629e5f98e53a7", - "bytes": 7279 + "sha256": "aa141c01fa78ac53c9b7b48917811262b012b5fb2bac40fa23afc4d8b8d6fab0", + "bytes": 7298 }, { "path": "apps/web/src/components/AnswerBody.tsx", - "sha256": "87396d1536d00c5a41fdb6743c0ab3fb81a769c3fae9afcf24d9d7cd7148af76", - "bytes": 1676 + "sha256": "2585378ff73ce37e67619e1ef98578d97d44b582514293386482c5234dd23b17", + "bytes": 1698 }, { "path": "apps/web/src/components/AnswerCitations.test.tsx", - "sha256": "56f1847f0bc524654cbf6c93124f55d60240be9091b4851788aff44c2738cf27", - "bytes": 6095 + "sha256": "e99ef91de9538f951746e7ad686eade831bbcf9c1a46a4fb4aebff9cbd79ea84", + "bytes": 6503 }, { "path": "apps/web/src/components/AnswerHold.tsx", - "sha256": "c2d3d09045ae9da9c822c4ae1a5d9fd9883b7296c5c1e4115f11729c69cf8420", - "bytes": 1253 + "sha256": "f1c97c24eb7d599af5d100dff4f4166e3e7aea66c3ad681a6884e172f4787b5b", + "bytes": 1473 }, { "path": "apps/web/src/components/AnswerReadability.test.tsx", @@ -673,8 +673,8 @@ }, { "path": "apps/web/src/components/Brand.tsx", - "sha256": "c30cfb4d067644d767300d337be89c245aca929deb74eb2aba3729d10f945bbf", - "bytes": 644 + "sha256": "a119e4bfd13767c9c3db3933c465ab0e899d0eb03aef3fc46804116378c391db", + "bytes": 706 }, { "path": "apps/web/src/components/CapabilityWorkbench.test.tsx", @@ -683,8 +683,8 @@ }, { "path": "apps/web/src/components/CapabilityWorkbench.tsx", - "sha256": "23d8fb63acd9a53cb93ec34a0cdd0d47f37cb7d49510d703496d10145fa82ed2", - "bytes": 7752 + "sha256": "3fb5fa29b51d9f96e1e7b5958029ee46dbfe5e88750ff6457988ca066c94cf48", + "bytes": 7655 }, { "path": "apps/web/src/components/CheckedObservations.test.tsx", @@ -733,8 +733,8 @@ }, { "path": "apps/web/src/components/ConfirmedFacts.tsx", - "sha256": "7d8682f120176cfa47ad9f34abae9ba3c5d0d1feba67141ee690edd8565975ed", - "bytes": 3102 + "sha256": "38448e96054a958fff7de6b013fba9c5544edabc889fa478abbd59dd2e46b7e1", + "bytes": 3155 }, { "path": "apps/web/src/components/ConnectFailureInWorkspace.test.tsx", @@ -743,18 +743,23 @@ }, { "path": "apps/web/src/components/ConnectedSourceCard.tsx", - "sha256": "5a3f35e0fab0ed63f7ee66152b353a5cf86ee2040d8921fe5267b4cf83b15b8e", - "bytes": 2147 + "sha256": "2b8a516f96ace07f1c6b567164158cf938b23e0a0c75cbccba66f8b43e200115", + "bytes": 2146 }, { "path": "apps/web/src/components/ContractV3Screens.test.tsx", "sha256": "cc58a42347636f4d117d503d4f7495606cff79c5541327049f6ff543104ffdc0", "bytes": 8555 }, + { + "path": "apps/web/src/components/ConversationEarlierRound.test.tsx", + "sha256": "4aca3030e5519fb03d2fad5b7ab7cedf50f64ce6b6cc2061bc788b687bc7d1e4", + "bytes": 4633 + }, { "path": "apps/web/src/components/ConversationTimeline.tsx", - "sha256": "fac6cb6df248655336792466ee4d4144c07f84345bc8c2fe9b57f6854b7b8b34", - "bytes": 13459 + "sha256": "dc4a30fa2bced151ee36dc7098c48c3dd790d1fee8c5477d727ea625874d0296", + "bytes": 13850 }, { "path": "apps/web/src/components/ConversationTimelineAutoRetry.test.tsx", @@ -798,8 +803,8 @@ }, { "path": "apps/web/src/components/DomainRecords.tsx", - "sha256": "c70020a94bc96d7c47f32b366af3e77c4b80fbd848b96ce88bdedd53bf178022", - "bytes": 3936 + "sha256": "19752aee19b8894a20df5c6ab8a84aa681aec53fd0391f628780cecaceab34ef", + "bytes": 3827 }, { "path": "apps/web/src/components/EffectDeclarationFields.tsx", @@ -813,8 +818,8 @@ }, { "path": "apps/web/src/components/EvidenceRail.tsx", - "sha256": "570f616f7084665f4afede470d23cd3af537bb9f1100e2cd80d9cc7e96e0fc81", - "bytes": 5208 + "sha256": "bfea1fc7080953fd63d72054a5e34e9708c060689a1427b2f1d5d3648adf3c66", + "bytes": 5112 }, { "path": "apps/web/src/components/EvidenceUseSummary.tsx", @@ -843,8 +848,8 @@ }, { "path": "apps/web/src/components/FileConnectionPanel.tsx", - "sha256": "e40358b3d92c64125d9598c51de0db8f67b8cd71be049b64679e964a09785e44", - "bytes": 10937 + "sha256": "f3293ac86d597a6612a008625dfd220b85d736c5dd08a1a67e3c3eff8fea8eb0", + "bytes": 10777 }, { "path": "apps/web/src/components/FileConnectionPanelParserError.test.tsx", @@ -973,8 +978,8 @@ }, { "path": "apps/web/src/components/LiveProjectWorkspace.tsx", - "sha256": "eaba759078b351030d8f3e33938e6781c7d5a3d493dc89d73cc0c1fad5f76170", - "bytes": 28294 + "sha256": "12e5701eb723a065204cef20d37476503d8125be00d5c087dff4b1e458deaa0d", + "bytes": 28033 }, { "path": "apps/web/src/components/LiveProjectWorkspaceConsent.test.tsx", @@ -993,8 +998,8 @@ }, { "path": "apps/web/src/components/LiveProjectWorkspaceRestart.test.tsx", - "sha256": "81b554af04f58f425130e922fce11d93cb6837bd3c7b28ab9592cac4b7b23610", - "bytes": 42955 + "sha256": "c2acb9dfd8885153a4a3392090ce742ed999b5c1a63adca5da024749a6e1abc0", + "bytes": 43945 }, { "path": "apps/web/src/components/LocalRetrievalSettingsCard.test.tsx", @@ -1053,18 +1058,18 @@ }, { "path": "apps/web/src/components/ModelSettings.test.tsx", - "sha256": "2482b76f2217307f51d132bd7682ed726ed4a40bb626d03ad1c0e7b53a73ec91", - "bytes": 28696 + "sha256": "0b7dcfe90074e28a61dae40fe6a38c1a88ab8a6fbebd2fe08ae87870f560507e", + "bytes": 28719 }, { "path": "apps/web/src/components/ModelSettings.tsx", - "sha256": "8f65487fb10690176b9bd5b2cdccf18615f9a8086625c532ed24a97fe050062c", - "bytes": 10717 + "sha256": "0757d29a8dcddc0302d93749b7b3395766e44fde1131ae14a022dc5a857cf104", + "bytes": 10724 }, { "path": "apps/web/src/components/NavigationDrawer.test.tsx", - "sha256": "7fb0866c71df0cad35946a0f6e7cf81eef101893f665ae1fd93de9750aaeee05", - "bytes": 11618 + "sha256": "06a669a705a06426260af646f6160fab438411c3264142d2d66c4829e5499f26", + "bytes": 11617 }, { "path": "apps/web/src/components/OAuthLoginFlow.tsx", @@ -1083,13 +1088,13 @@ }, { "path": "apps/web/src/components/ProjectForm.test.tsx", - "sha256": "070d13dc35cbb44922f48f503a7b760813f8f1789c56c96494691ee2c53c0e3c", - "bytes": 1608 + "sha256": "6bcf342d5e95785ffa4970491fdd39f219b43bc131ac266896406a247c04d154", + "bytes": 1607 }, { "path": "apps/web/src/components/ProjectForm.tsx", - "sha256": "5d1764bbd5a0e00ca79e54e2e567e4bdd0024fd44d00049884718019e80267de", - "bytes": 3839 + "sha256": "9e767d7ebe9428945219d1a34d9772a44c6584ad921fa00a4b2a20497dcef33f", + "bytes": 3838 }, { "path": "apps/web/src/components/ProjectLanding.test.tsx", @@ -1133,8 +1138,8 @@ }, { "path": "apps/web/src/components/ProjectSessionPages.tsx", - "sha256": "10297ae274e340a734474fe4ac873d6d49a0c9bcb95bb8b9b2a06bc54039a845", - "bytes": 10850 + "sha256": "3ac79479989184e9a69b18ef3e99f41adde096a01c9c7301a52e13bf8808d083", + "bytes": 10736 }, { "path": "apps/web/src/components/ProjectWebSetup.test.tsx", @@ -1238,8 +1243,8 @@ }, { "path": "apps/web/src/components/ResearchResultCard.tsx", - "sha256": "46a2f696960986d294c97e55ec6250fd91a91bc47ca756d372e8cef706c22ddd", - "bytes": 9398 + "sha256": "2b0c9f3492f68b79f12cc64ed370d439dc925577175da94b1aaf0e0574223813", + "bytes": 9512 }, { "path": "apps/web/src/components/ResearchSidePanel.tsx", @@ -1248,13 +1253,13 @@ }, { "path": "apps/web/src/components/ResourcesPage.test.tsx", - "sha256": "2e827a36757498a1df4bf07bddbcffc383cdf9829531b695338f53a1457aa57d", - "bytes": 6748 + "sha256": "e867dba8d2646895313bdfebe65244abf38ce6a469a0baad355a6d9f93c3328e", + "bytes": 6747 }, { "path": "apps/web/src/components/ResultCardOnce.test.tsx", - "sha256": "783a5bbb3819301c5aea8e5408ff723e479f3d3dea947a240ae879317e58038c", - "bytes": 13346 + "sha256": "0b9ddeb1633417558b061bbd50eaf2becc0f54daa2ccf6487da1c91b83fbeb54", + "bytes": 13886 }, { "path": "apps/web/src/components/ResultEvidencePanel.tsx", @@ -1268,8 +1273,8 @@ }, { "path": "apps/web/src/components/ResultHoldDetails.tsx", - "sha256": "df563dab05fdcd5a01a93cb4b03fab892d7980506eb5734d373f93a42301b8ca", - "bytes": 3577 + "sha256": "6b378960876ac027cf61328b3fd9fdf4b35a57b6927f0e04ec71b081b13969e4", + "bytes": 3742 }, { "path": "apps/web/src/components/ResultInterruptionNotice.test.tsx", @@ -1316,6 +1321,16 @@ "sha256": "7903291351348081e58962e3589f63a929ee70e3f3b53d2249486a0bc897b056", "bytes": 4538 }, + { + "path": "apps/web/src/components/Select.test.tsx", + "sha256": "5541d260758ef0a8cee89ddb461848ed89c0ae33118fcd8f832c950165e9c900", + "bytes": 1353 + }, + { + "path": "apps/web/src/components/Select.tsx", + "sha256": "4da7d2e6a180c885d14d51e7aab538def5eaa6c8691063a318ed9fd25edeeb30", + "bytes": 625 + }, { "path": "apps/web/src/components/Sentences.test.tsx", "sha256": "ea1a23a11154e20d43e42bb7fb98778998c4708637feb2a6d07e8391446d56a2", @@ -1343,8 +1358,8 @@ }, { "path": "apps/web/src/components/SourceTimeAdvanced.tsx", - "sha256": "f686c4d7cb31caee10fa99ffefb393e436037530d4082465f4b016db339d9435", - "bytes": 3742 + "sha256": "0683561a5721cd80fee8e3269bef1eef49602679caffc31d89319cb413126db5", + "bytes": 3761 }, { "path": "apps/web/src/components/SourceTimeConfirmationDialog.tsx", @@ -1493,8 +1508,8 @@ }, { "path": "apps/web/src/components/WorkspaceTopbar.tsx", - "sha256": "ac9075e74c3802b1153498c6012aaeff44268772d03d8435d39dc00e7470e6c8", - "bytes": 3697 + "sha256": "5ad5cf984d158091501c56d51fee1096e4eda7e8cb0ff64d6082056a1d25f35b", + "bytes": 3696 }, { "path": "apps/web/src/components/XaiDeviceLogin.tsx", @@ -1658,8 +1673,8 @@ }, { "path": "apps/web/src/components/history/ResearchHistoryWorkspace.tsx", - "sha256": "2d4c02e54a6881ca7566fe422a673dfd585080f300887a3b7e9fd7ee630b6bc0", - "bytes": 11236 + "sha256": "1c0b7f73e066c4428df11d6296c7efef9e21efe3e98abc580bb19f0e779daf25", + "bytes": 11127 }, { "path": "apps/web/src/components/history/RestorePreviewDialog.tsx", @@ -1803,8 +1818,8 @@ }, { "path": "apps/web/src/components/statusLabels.test.tsx", - "sha256": "87fb0355ac6330c1736a59f6f94b0a8a05630e9ccbbd48beded5e09f120bfc64", - "bytes": 2642 + "sha256": "ca6ff4d5c735e2da417140c0621df722669681f36559b92cc077cd73ccc3715d", + "bytes": 2656 }, { "path": "apps/web/src/components/statusLabels.ts", @@ -1928,8 +1943,8 @@ }, { "path": "apps/web/src/components/useNavigationDrawer.ts", - "sha256": "29fddd758ba9646a2e471fb41e92eb28ec2469c8722a32d92221f5db07c3afae", - "bytes": 2324 + "sha256": "6c4fbaf3ae858673ed4844a738d28373c686ea7e941ccf7478f611c3925213e7", + "bytes": 2323 }, { "path": "apps/web/src/components/useOpenAtLatestTurn.test.tsx", @@ -2013,8 +2028,8 @@ }, { "path": "apps/web/src/i18n/apiNoticesMessages.ts", - "sha256": "3ba1dd571e9d90553c52e6960011eadfac557274964a60d078d0727ec024654f", - "bytes": 5433 + "sha256": "91437790ffadd57ec2dbd2fa27ed8ec4829d2ecac683e747295fa33945142e10", + "bytes": 5703 }, { "path": "apps/web/src/i18n/bilingualRecord.ts", @@ -2038,8 +2053,8 @@ }, { "path": "apps/web/src/i18n/countFormatMessages.ts", - "sha256": "27b8a6cdc4bd769f2fec9be20dfd8488996dd548706c77d4839af5325edd4cb7", - "bytes": 15890 + "sha256": "0e98dad151c5957478089a6385a09187341ee350303485c503df2433c607d3a4", + "bytes": 15873 }, { "path": "apps/web/src/i18n/credentialMessages.ts", @@ -2058,7 +2073,7 @@ }, { "path": "apps/web/src/i18n/formatMessages.ts", - "sha256": "ccbb8177183c287db993e52fb3cd0b4599dabc41f1eb6773159f3481e0b06e04", + "sha256": "40d9cc45b3273cb3f31151e622a745327650548df4758f7119ec9a993995eedc", "bytes": 9435 }, { @@ -2108,8 +2123,8 @@ }, { "path": "apps/web/src/i18n/ja/apiNoticesMessagesJa.ts", - "sha256": "fcefdb580895c5956b4b62c2f80d8e284ba594e64cecbfa44232d8ba54bb5d21", - "bytes": 3282 + "sha256": "e24784808bd10e8055d1e3db7100de1b8568bb2315b7c6a663ce6fd4158add25", + "bytes": 3444 }, { "path": "apps/web/src/i18n/ja/commonStateMessagesJa.ts", @@ -2213,8 +2228,8 @@ }, { "path": "apps/web/src/i18n/ja/sourceMessagesJa.ts", - "sha256": "46e1ab3bf54406a41a884d016eba666de66731ceed7824e70873a1da6e0a6c52", - "bytes": 7561 + "sha256": "cce728a983fc6743a63d3f5bcaa944901261ef5748218854b7e454c8d11fa007", + "bytes": 7478 }, { "path": "apps/web/src/i18n/ja/traceMessagesJa.ts", @@ -2223,13 +2238,13 @@ }, { "path": "apps/web/src/i18n/ja/workspaceDetailMessagesJa.ts", - "sha256": "a8eb7c0034c0d5c39bf0963235990991b6ea4805aa3a7483f38153bd43f0e0f3", - "bytes": 2855 + "sha256": "d68a6d078258343eb6a5326b21a4f3097d3d707195a22b2e2c3243fa4f3b67a4", + "bytes": 2754 }, { "path": "apps/web/src/i18n/ja/workspaceMessagesJa.ts", - "sha256": "8d94ed5f4d8c90514b0bf4bdc11bc230e2e6844f2b94516c2d21bf6626ea75f5", - "bytes": 9823 + "sha256": "213e1cf46aedd471a9ce95b50d78fef67aa2aa7587cb3aa0725a731cbed8e2f4", + "bytes": 9621 }, { "path": "apps/web/src/i18n/jaScreen.test.ts", @@ -2363,8 +2378,8 @@ }, { "path": "apps/web/src/i18n/sourceMessages.ts", - "sha256": "5030d7a6e025496f03c72290402394b5bbe7698d8a58dc00888d1946d12043c1", - "bytes": 11986 + "sha256": "8e69990033fe21b6cab89fd29b058f62a00972c4738b21ca7e476044b24587a0", + "bytes": 11850 }, { "path": "apps/web/src/i18n/stateFormatMessages.ts", @@ -2373,28 +2388,28 @@ }, { "path": "apps/web/src/i18n/statusChinese.ts", - "sha256": "7feb330c61f6662b4e891f17cf198ac2ec3b60eb8ed86b1975f121b804e87325", - "bytes": 5275 + "sha256": "beb99204454e2a8341fb4843949c1b6e8ff77f4a0760da2c34ffc8f7d0fe1d3d", + "bytes": 5043 }, { "path": "apps/web/src/i18n/statusEnglish.ts", - "sha256": "6dac000931f26ad7edb83f9d8f544ca5f852f7aa668ca43fdea5e7614d007b8a", - "bytes": 5526 + "sha256": "caf375733d39ad9e9cd9da57fb770b0320c12fec2cb373c65f780099f7edb041", + "bytes": 5282 }, { "path": "apps/web/src/i18n/statusJapanese.ts", - "sha256": "ff09b8f2d71049724a91efd8e0f649f5e743c4f61480cd76391fb871e3d6a4c2", - "bytes": 6074 + "sha256": "e52df8d27d736da76c0e51d8209ba3a11d3f088c586fd9eb0972d4c02e146c49", + "bytes": 5812 }, { "path": "apps/web/src/i18n/statusResources.ts", - "sha256": "7cfa0b6fd0aac8e97cc2ca12047c468dd922d05c660bac6cf0573ae2aee6b38e", - "bytes": 6823 + "sha256": "6ccc34dff1bfd6c10dbbbcfdec2997705e058c3f6eef9e9d182134a0130cff56", + "bytes": 6592 }, { "path": "apps/web/src/i18n/statusTraditional.ts", - "sha256": "033c1acec5e608d605246c02bb77dc90d1d4211b86f91b30f433904c2d7924c6", - "bytes": 5279 + "sha256": "7233017698241aa41524877c9386adb84e2531107264b3c85ad8b24d84d3513c", + "bytes": 5047 }, { "path": "apps/web/src/i18n/testOrderResources.ts", @@ -2448,18 +2463,18 @@ }, { "path": "apps/web/src/i18n/wordingRules.test.ts", - "sha256": "8e3102e3de9c6e24ff70fc8f37205db1612d0f77333783c7f973968b3c74fc8b", - "bytes": 10468 + "sha256": "c1123ae5a3befebc886159ec32dc0037012a106fd190dee2c1ca9a7d6cc6e3e2", + "bytes": 13078 }, { "path": "apps/web/src/i18n/workspaceDetailMessages.ts", - "sha256": "5bc7df63f4c8b1b8dc9b497c14c632dc5970c31af716cd34f8bb24ad799b0f86", - "bytes": 4198 + "sha256": "5b4e5950f9c30b32f331ea09673efaf4dfcea273b9b9b17e5d28d57e5c09c5e4", + "bytes": 4031 }, { "path": "apps/web/src/i18n/workspaceMessages.ts", - "sha256": "af89ca11c886b071b88ef155bdc1bcc32ddcff31aa7e45fa4cd7ea97848b4a4b", - "bytes": 16055 + "sha256": "74cf1cf623915a5e6ce1bc39d6ed8c89540795f084683099198d1ba8a713f45d", + "bytes": 15708 }, { "path": "apps/web/src/i18n/zh/activityMessagesZh.ts", @@ -2473,8 +2488,8 @@ }, { "path": "apps/web/src/i18n/zh/apiNoticesMessagesZh.ts", - "sha256": "eaff16743e63e01a29d55845472c0825dc6d6649e7ec06050936c0263a1bfaf4", - "bytes": 2537 + "sha256": "0708bf415de42d38033f94c796b1b3e250911e67843e9b37aabfb5e7d98f2020", + "bytes": 2663 }, { "path": "apps/web/src/i18n/zh/commonStateMessagesZh.ts", @@ -2578,8 +2593,8 @@ }, { "path": "apps/web/src/i18n/zh/sourceMessagesZh.ts", - "sha256": "543014a38dd33e23887db4a632a74553d09791e8f8f80fcda11553688e28cf17", - "bytes": 5379 + "sha256": "b60b448c8dcebcc60fb8ed8923ca91e8ff99bb6a32bcb39778df181f2e91c5ba", + "bytes": 5323 }, { "path": "apps/web/src/i18n/zh/traceMessagesZh.ts", @@ -2588,13 +2603,13 @@ }, { "path": "apps/web/src/i18n/zh/workspaceDetailMessagesZh.ts", - "sha256": "8602dd90df6301c6b4c528ec4716dfca51c5c524ea05822febad381599133c58", - "bytes": 2090 + "sha256": "75c523a6d22f8c4a9c59cd8b51103df430a85d2d151136ac82fbbf164884ee99", + "bytes": 2007 }, { "path": "apps/web/src/i18n/zh/workspaceMessagesZh.ts", - "sha256": "0baa8b48bfd7a08c043f16f8f7f959696c3d27bfb50bf54dafef9a396736e310", - "bytes": 7631 + "sha256": "6562f00fb08f60eab15cd5595dc324887af7578c9988c34d61f3ab75ac621c20", + "bytes": 7480 }, { "path": "apps/web/src/i18n/zhHant/activityMessagesZhHant.ts", @@ -2608,8 +2623,8 @@ }, { "path": "apps/web/src/i18n/zhHant/apiNoticesMessagesZhHant.ts", - "sha256": "1e175c1ede81021bb4bb5a151393f0041f6b698c11569affef409031402eeef2", - "bytes": 2541 + "sha256": "56aa8eacb6c8c88fe09895e71bee4eb5679fe6da1e686ada31395670717c53e3", + "bytes": 2667 }, { "path": "apps/web/src/i18n/zhHant/commonStateMessagesZhHant.ts", @@ -2713,8 +2728,8 @@ }, { "path": "apps/web/src/i18n/zhHant/sourceMessagesZhHant.ts", - "sha256": "795a604a027b1d0b11410dbbc358d0bee62ae988950925fafba9f92b51312eb4", - "bytes": 5392 + "sha256": "308f999549c2cb9eda5ee0f550e98612ee2f76403f061c7386fa54a4b8eab6e1", + "bytes": 5336 }, { "path": "apps/web/src/i18n/zhHant/traceMessagesZhHant.ts", @@ -2723,13 +2738,13 @@ }, { "path": "apps/web/src/i18n/zhHant/workspaceDetailMessagesZhHant.ts", - "sha256": "ed053f2fe179e13cb11a3b84bf56e158136354c7708a545adb14b7c4c96dbe3d", - "bytes": 2088 + "sha256": "29dd70f4e7ecbf071604177f8d2a5c861dc88c6aebdad314769b4bc35833d92f", + "bytes": 2005 }, { "path": "apps/web/src/i18n/zhHant/workspaceMessagesZhHant.ts", - "sha256": "575282047e728fc53214b69a79606e964a6c017324ed93de1455f8b2bd39baf1", - "bytes": 7665 + "sha256": "e08758b72bb82891b0f88805e02db8053a123401ba235a99ef2939fd97de8793", + "bytes": 7514 }, { "path": "apps/web/src/i18n/zhHantScreen.test.ts", @@ -2748,8 +2763,8 @@ }, { "path": "apps/web/src/narrowLayoutRules.test.ts", - "sha256": "ed05cbc9cf5895079bb33b327fcb731e16eb3d515a0b7327591d7d8d5beb4559", - "bytes": 5845 + "sha256": "e1ce29dc550b80197befd2067339f6d36d08d341218a9337e3ab722d38bc1761", + "bytes": 6689 }, { "path": "apps/web/src/preview-timeline.tsx", @@ -2788,8 +2803,8 @@ }, { "path": "apps/web/src/typeScaleRules.test.ts", - "sha256": "5c68854bcb2a2347d677a20904ca87d1cee23eb295b70e5b74e19f7b85825737", - "bytes": 1921 + "sha256": "144e34be972db8f63f42bea91cafd73cb81d41b9abb9e45f6ac476ad8365212a", + "bytes": 1920 }, { "path": "apps/web/src/types.ts", @@ -2803,8 +2818,8 @@ }, { "path": "apps/web/src/workstation.css", - "sha256": "7e9842f7f1d5020f79ba299d8bf42d96adba4cabff72927d865570a66fb6c6d7", - "bytes": 74239 + "sha256": "ed5fe3509cb88b96ee93264fb467a67a0192c0002ed8802b129a1dae74fc498d", + "bytes": 74820 }, { "path": "apps/web/timeline-preview.html", diff --git a/apps/web/src/api/resultEvidence.ts b/apps/web/src/api/resultEvidence.ts index d4c0f253..71644eb0 100644 --- a/apps/web/src/api/resultEvidence.ts +++ b/apps/web/src/api/resultEvidence.ts @@ -66,9 +66,13 @@ function bindAnswer(selection: ResultSelection, displayed: Record Object.hasOwn(resultBody, key))) mismatch(); + // The latest round is shown from the summary view. A saved earlier round is shown from the stored result itself + // (operation/result/read returns it whole). What the screen shows must be, all of it, a part of one of these forms + // of the same stored answer; keys taken from two forms that disagree are refused. + const shownIsStored = [shownBody, resultBody, manifestBody].some(body => isShownPartOf(displayedContent, body)); if (manifest.operation_id !== selection.operationId || manifest.request_ref.project_id !== scope.projectId || manifest.request_ref.entity_type !== "THREAD" || manifest.request_ref.entity_id !== `request:${thread}` || manifest.request_ref.revision_digest !== scope.requestDigest || - resultContentKey(without(resultBody, projected)) !== resultContentKey(without(manifestBody, projected)) || !isShownPartOf(displayedContent, shownBody)) mismatch(); + resultContentKey(without(resultBody, projected)) !== resultContentKey(without(manifestBody, projected)) || !shownIsStored) mismatch(); } async function read(selection: ResultSelection, displayed: Record, digest: string | undefined, offset: number, expand: boolean, signal?: AbortSignal) { diff --git a/apps/web/src/api/resultEvidenceRuntime.test.ts b/apps/web/src/api/resultEvidenceRuntime.test.ts index 42d03cf5..efd43341 100644 --- a/apps/web/src/api/resultEvidenceRuntime.test.ts +++ b/apps/web/src/api/resultEvidenceRuntime.test.ts @@ -46,6 +46,26 @@ describe.each(["stored_current", "stored_before_grounded_rows"] as const)("the a serve(c, value => { value.result = { ...value.result, answer: "another answer" }; value.shown_result = { ...value.shown_result, answer: "another answer" }; }); await expect(show(c, { ...c.displayed, answer: "another answer" })).rejects.toThrow(BINDING); }); + // An earlier round of a conversation is shown from operation/result/read, which returns the stored result whole, + // not the summary view the latest round is shown from. + it("opens the evidence for an earlier round, which the screen shows from the stored result itself", async () => { + serve(c); + const whole = c.read.result as Wire; + // it is not the summary view: at least one of the two summarized records is there in full + expect(["absence_assessments", "checked_observations"].some(key => JSON.stringify(whole[key]) !== JSON.stringify(c.displayed[key]))).toBe(true); + expect((await show(c, whole)).selected_count).toBeGreaterThan(0); + expect((await show(c, c.read.manifest.result as Wire)).selected_count).toBeGreaterThan(0); + }); + it("still rejects a whole result with another answer or an invented key, and one put together from two forms", async () => { + serve(c); + const whole = c.read.result as Wire; + await expect(show(c, { ...whole, answer: "another answer" })).rejects.toThrow(BINDING); + await expect(show(c, { ...whole, invented_key: 1 })).rejects.toThrow(BINDING); + // the summary of one record inside the whole result: each key is right in one form, the whole is right in none + const mixedKey = (["absence_assessments", "checked_observations"] as const).find(key => JSON.stringify(whole[key]) !== JSON.stringify(c.displayed[key])); + expect(mixedKey).toBeDefined(); + await expect(show(c, { ...whole, [mixedKey!]: c.displayed[mixedKey!] })).rejects.toThrow(BINDING); + }); }); describe("a server that does not send shown_result or projected_keys (older)", () => { diff --git a/apps/web/src/api/uploadTypes.ts b/apps/web/src/api/uploadTypes.ts index 4bdfbc37..90ce30db 100644 --- a/apps/web/src/api/uploadTypes.ts +++ b/apps/web/src/api/uploadTypes.ts @@ -44,4 +44,3 @@ export function refusalBeforeUpload(file: Pick): UploadRe if (file.size > MAX_UPLOAD_BYTES) return new UploadRefused("UPLOAD_TOO_LARGE", file.name); return null; } - diff --git a/apps/web/src/components/ActionCompare.tsx b/apps/web/src/components/ActionCompare.tsx index 71059e67..dccbd66c 100644 --- a/apps/web/src/components/ActionCompare.tsx +++ b/apps/web/src/components/ActionCompare.tsx @@ -50,7 +50,7 @@ export function ActionCompare({ result, projectId, links, discrimination }: { const evaluations = objectList(analysis.evaluations); const extra = [...actions.flatMap(action => stringValues(action.source_refs)), ...evaluations.flatMap(item => stringValues(item.evidence_refs))]; const numbers = numberSources(typeof result.answer === "string" ? result.answer : "", extra); - const cite = (refs: string[]) => refs.map(ref => [{numbers.get(ref)}]); + const cite = (refs: string[]) => refs.map(ref => [{numbers.get(ref)}]); if (actions.length === 0) return

{t("actions.empty")}

; const againstNote = (action: Record) => standingActionNote([...stringValues(action.hypothesis_ids), ...stringValues(action.hypothesis_refs)], discrimination); diff --git a/apps/web/src/components/ActionEffort.tsx b/apps/web/src/components/ActionEffort.tsx index 6cab4160..68d03155 100644 --- a/apps/web/src/components/ActionEffort.tsx +++ b/apps/web/src/components/ActionEffort.tsx @@ -3,7 +3,8 @@ import { useDraftGeneration, type DraftTicket } from "./useDraftGeneration"; import { useReadAccessProtection } from "./useReadAccessProtection"; import { message as formatMessage } from "../i18n/formatMessages"; import { t, useTranslation } from "../i18n/locale"; -import { Button, Callout, HTMLSelect, InputGroup } from "@blueprintjs/core"; +import { Button, Callout, InputGroup } from "@blueprintjs/core"; +import { Select } from "./Select"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useState } from "react"; import { objectValue, textValue } from "../api/presentation"; @@ -83,7 +84,7 @@ export function ActionEffort({ projectId, actionId, initial, legacy }: { project {open &&
{DIMENSIONS.map(dimension =>
)}
diff --git a/apps/web/src/components/FileConnectionPanel.tsx b/apps/web/src/components/FileConnectionPanel.tsx index 7709348b..c124bffd 100644 --- a/apps/web/src/components/FileConnectionPanel.tsx +++ b/apps/web/src/components/FileConnectionPanel.tsx @@ -1,13 +1,13 @@ import { message } from "../i18n/formatMessages"; import { t, useTranslation } from "../i18n/locale"; -import { Button, Callout, FileInput, HTMLSelect } from "@blueprintjs/core"; +import { Button, Callout, FileInput } from "@blueprintjs/core"; +import { Select } from "./Select"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useEffect, useRef, useState } from "react"; import { stageFile, type StagedFile, type UploadProgress } from "../api/files"; import { UiError } from "../i18n/UiError"; import { sentenceGap } from "../i18n/sentences"; import { rpc } from "../api/rpcClient"; -import { eyebrowLabel } from "./statusLabels"; import { authorityOptions, sourceConnectSchema } from "../api/sourcePolicy"; import { parserFailureText } from "../api/parserError"; import { refusalBeforeUpload, UPLOAD_ACCEPT, UploadRefused } from "../api/uploadTypes"; @@ -99,10 +99,10 @@ export function FileConnectionPanel({ projectId, hosted = false }: { projectId: } }); const progress = progressText(uploadProgress, connectingName); return
-

{eyebrowLabel("AUTHORIZED SOURCES")}

{t("sources.fileTitle")}

{t("sources.preserve")}
+

{t("sources.fileTitle")}

{t("sources.fileTypes")}{hosted?t("sources.hostedUpload"):t("sources.localUpload")}

{ selectionVersion.current += 1; setNotice(null); setUploadProgress(null); setConnectingName(""); setFiles(Array.from((event.target as HTMLInputElement).files ?? [])); }} /> - + ); } diff --git a/apps/web/src/components/LiveProjectWorkspace.tsx b/apps/web/src/components/LiveProjectWorkspace.tsx index b426686f..899b90ed 100644 --- a/apps/web/src/components/LiveProjectWorkspace.tsx +++ b/apps/web/src/components/LiveProjectWorkspace.tsx @@ -1,7 +1,7 @@ import { message } from "../i18n/formatMessages"; import { t, useTranslation } from "../i18n/locale"; import { sentenceGap } from "../i18n/sentences"; -import { Button, Callout, Icon, InputGroup, Tab, Tabs, Tag } from "@blueprintjs/core"; +import { Button, Callout, InputGroup, Tab, Tabs, Tag } from "@blueprintjs/core"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode, type RefObject } from "react"; import { flushSync } from "react-dom"; @@ -27,7 +27,6 @@ import { workspaceSetupIssue, workspaceSetupIssueText, type WorkspaceSetupIssue import { LanguageSettingsCard } from "./LanguageSettingsCard"; import { useResearchSession } from "./useResearchSession"; import { useReadAccessProtection } from "./useReadAccessProtection"; -import { eyebrowLabel } from "./statusLabels"; import { WorkspaceErrorBoundary } from "./WorkspaceErrorBoundary"; import { ConversationTimeline } from "./ConversationTimeline"; import { ArchivedProjects } from "./ArchivedProjects"; @@ -169,8 +168,8 @@ export function ProjectLanding({ projects, loading = false, contextRestoreIssue {sentenceGap()}{t("workspace.restoreScope")}; const setupNotice = setupIssue && {workspaceSetupIssueText(setupIssue)}{sentenceGap()}{t("workspace.existingRead")}; if (creating) return <>{setupNotice}{restoreNotice}; - return
{setupNotice}{restoreNotice}

{eyebrowLabel("RESEARCH WORKSPACE")}

{t("workspace.continueHeading")}

{t("workspace.openOrCreate")}

-

{t("workspace.projects")}

+ return
{setupNotice}{restoreNotice} +

{t("workspace.projects")}

{loading&&

{t("workspace.loadingProjects")}

} {projects.map(project=>)}
diff --git a/apps/web/src/components/LiveProjectWorkspaceRestart.test.tsx b/apps/web/src/components/LiveProjectWorkspaceRestart.test.tsx index 38f2a257..9d6d3454 100644 --- a/apps/web/src/components/LiveProjectWorkspaceRestart.test.tsx +++ b/apps/web/src/components/LiveProjectWorkspaceRestart.test.tsx @@ -542,6 +542,20 @@ it("opens the project at its most recently updated work and shows the project cu expect(container.textContent).not.toMatch(GATE_CHECKING); }); +it("starts the home screen with the list of projects under a plain title, without a headline about the product", async () => { + await mount(); await settle(() => Boolean(container.querySelector(".project-home .project-home-item"))); + const home = container.querySelector(".project-home")!; + expect([...home.querySelectorAll("h1")].map(heading => heading.textContent)).toEqual(["프로젝트"]); + // the list is the first thing on the page and is named by that title + const list = home.querySelector("section.project-home-list")!; + expect(list.getAttribute("aria-labelledby")).toBe(home.querySelector("h1")!.id); + expect(home.querySelector(".eyebrow")).toBeNull(); + expect(home.querySelector(".workspace-heading")).toBeNull(); + expect(home.textContent).not.toContain("연구를 이어가거나"); + // one filled button: the way to a new project + expect([...home.querySelectorAll("button.bp6-intent-primary")].map(button => button.textContent)).toEqual(["새 프로젝트"]); +}); + it("keeps the new-session screen when the user chooses 새 대화 after opening a project", async () => { fixture.executionReady = true; await mount(); await settle(() => Boolean(container.querySelector(".project-button"))); diff --git a/apps/web/src/components/ModelSettings.test.tsx b/apps/web/src/components/ModelSettings.test.tsx index 243be6e5..72876da5 100644 --- a/apps/web/src/components/ModelSettings.test.tsx +++ b/apps/web/src/components/ModelSettings.test.tsx @@ -393,7 +393,8 @@ it("shows this project's settings on the project tab and this computer's from th await openPage("설정"); const titles = () => [...container.querySelectorAll(".workspace-content h2")].map(item => item.textContent); expect(container.querySelector(".workspace-heading h1")?.textContent).toBe("프로젝트 설정"); - expect(container.querySelector(".workspace-heading .eyebrow")?.textContent).toBe("이 프로젝트"); + // the title says the scope once; no small label above it says it again + expect(container.querySelector(".workspace-heading .eyebrow")).toBeNull(); const projectTitles = titles(); expect(projectTitles).toContain("모델 기본값"); expect(projectTitles).not.toContain("모델 연결"); @@ -401,7 +402,7 @@ it("shows this project's settings on the project tab and this computer's from th await act(async () => { ([...container.querySelectorAll(".sidebar-bottom button")].find(item => item.textContent === "이 컴퓨터 설정") as HTMLButtonElement).click(); }); await flush(); await flush(); expect(container.querySelector(".workspace-heading h1")?.textContent).toBe("이 컴퓨터 설정"); - expect(container.querySelector(".workspace-heading .eyebrow")?.textContent).toBe("이 컴퓨터"); + expect(container.querySelector(".workspace-heading .eyebrow")).toBeNull(); const computerTitles = titles(); expect(computerTitles).toContain("모델 연결"); expect(computerTitles).toContain("언어"); diff --git a/apps/web/src/components/ModelSettings.tsx b/apps/web/src/components/ModelSettings.tsx index 64c523e3..1ef3ba02 100644 --- a/apps/web/src/components/ModelSettings.tsx +++ b/apps/web/src/components/ModelSettings.tsx @@ -1,7 +1,8 @@ import { message } from "../i18n/formatMessages"; import { t, useTranslation } from "../i18n/locale"; import { joinSentences, sentenceGap } from "../i18n/sentences"; -import { Button, HTMLSelect } from "@blueprintjs/core"; +import { Button } from "@blueprintjs/core"; +import { Select } from "./Select"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useState } from "react"; import { rpc } from "../api/rpcClient"; @@ -135,16 +136,16 @@ export function ModelSettings({ projectId, threadId, selection, onSelect, onSave : inherited.error ? {t("model.parentFailed")} : {message("inheritedSelection", inheritedLabel ?? t("model.noProjectModel"))})} {inheritsProject && data?.availability !== "UNAVAILABLE" && {t("model.inheritsProject")}} - + {!compact &&
); } - diff --git a/apps/web/src/components/ProjectSessionPages.tsx b/apps/web/src/components/ProjectSessionPages.tsx index aea06e23..a56bf45f 100644 --- a/apps/web/src/components/ProjectSessionPages.tsx +++ b/apps/web/src/components/ProjectSessionPages.tsx @@ -96,7 +96,7 @@ function ProjectModelDefault({ projectId }: { projectId: string }) { function ProjectSettingsPage({ project, threadId, workspaceId, session, capturedEpoch, status, onPage, onOpenCatalog, modelConnected }: SettingsProps) { return <>
-

{t("settings.thisProject")}

{t("settings.projectTitle")}

{t("settings.projectNotice")}

+

{t("settings.projectTitle")}

{t("settings.projectNotice")}

@@ -127,7 +127,7 @@ export function ComputerSettingsPage({ credentialProjectId, workspaceId, storage }) { return <>
-

{eyebrowLabel("LOCAL WORKSPACE")}

{t("settings.computerTitle")}

{t("settings.computerNotice")}

+

{t("settings.computerTitle")}

{t("settings.computerNotice")}

{t("workspace.connectModel")}

diff --git a/apps/web/src/components/ResearchResultCard.tsx b/apps/web/src/components/ResearchResultCard.tsx index 9a7a4a4d..e6c8f0bd 100644 --- a/apps/web/src/components/ResearchResultCard.tsx +++ b/apps/web/src/components/ResearchResultCard.tsx @@ -33,7 +33,7 @@ import { ResultHoldDetails } from "./ResultHoldDetails"; * One research result, read top to bottom: state tags next to the heading, the answer, the detail buttons, then why * the answer is in that state (folding), the alternative explanations and the next actions. */ -export function ResearchResultCard({ result, state, unavailable, error, failure, terminalReason, onDetail, onRetry, onResume, resumeCompleted, resuming, historySelection, currentness, progressSummary, coverageMatrix, nextUserAction, decisionDelta, usageLine, executionHold, projectId, holdCauseRef, onOpenTrace, hypothesisLinks, linkDistribution, hasSavedResult = false, autoRetryOff, onOpenSettings }: { +export function ResearchResultCard({ result, state, unavailable, error, failure, terminalReason, onDetail, onRetry, onResume, resumeCompleted, resuming, historySelection, currentness, progressSummary, coverageMatrix, nextUserAction, decisionDelta, usageLine, executionHold, projectId, holdCauseRef, onOpenTrace, hypothesisLinks, linkDistribution, hasSavedResult = false, autoRetryOff, onOpenSettings, earlierRound }: { result: Record | null; state: string; unavailable?: string; error?:ExecutionError | null; failure?:ResearchFailure | null; terminalReason?:string | null; onDetail: (detail: ResearchDetail) => void; onRetry?: () => void; /** Continue the interrupted run from where it stopped; only offered with the number of stages it had finished. */ onResume?: () => void; resumeCompleted?: number; resuming?: boolean; historySelection?: HistorySelection; currentness?: Currentness; @@ -47,6 +47,8 @@ export function ResearchResultCard({ result, state, unavailable, error, failure, hasSavedResult?: boolean; /** The project's automatic retry of an interrupted model call is off (read by the timeline, which has the query client). */ autoRetryOff?: boolean; onOpenSettings?: () => void; + /** A saved round before the latest one. */ + earlierRound?: boolean; }) { useTranslation(); const languageCheck = objectValue(result?.hypothesis_language_check); @@ -80,7 +82,7 @@ export function ResearchResultCard({ result, state, unavailable, error, failure, ] as const).map(([kind,label])=>)}
{executionHold && projectId && } - + {Object.hasOwn(objectValue(result.portfolio), "hypotheses") &&

{t("result.alternatives")}

{causeUnconfirmed(result) && } {hypotheses.length ? hypotheses.map((h,index)=>

{textValue(h.statement)}

{textValue(h.uncertainty)&&{textValue(h.uncertainty)}}{projectId&&item.hypothesis_id===textValue(h.hypothesis_id))} all={hypothesisLinks} distribution={linkDistribution}/>}
) :

{t("result.noHypotheses")}

} diff --git a/apps/web/src/components/ResourcesPage.test.tsx b/apps/web/src/components/ResourcesPage.test.tsx index 1720b8ad..cda754f5 100644 --- a/apps/web/src/components/ResourcesPage.test.tsx +++ b/apps/web/src/components/ResourcesPage.test.tsx @@ -110,4 +110,3 @@ it("offers no way to the setting where this screen cannot change it (hosted revi expect(web.textContent).not.toContain("허용하면 여기서"); expect(web.querySelectorAll("button")).toHaveLength(0); }); - diff --git a/apps/web/src/components/ResultCardOnce.test.tsx b/apps/web/src/components/ResultCardOnce.test.tsx index b693c43c..cd870fb6 100644 --- a/apps/web/src/components/ResultCardOnce.test.tsx +++ b/apps/web/src/components/ResultCardOnce.test.tsx @@ -112,6 +112,14 @@ it("says the missing answer checks in the body, because no header tag can", () = expect(html).not.toContain("답변 점검 0개"); }); +it("says of an earlier round that its answer checks are not shown here, not that they have not arrived yet", () => { + const html = render(withOpen("PARTIAL_HOLD"), { earlierRound: true }); + expect(html).toContain("답할 수 있는 범위: 이 회차의 답변 점검 항목은 여기서 볼 수 없습니다. 최신 회차의 답에서만 보입니다."); + expect(html).not.toContain("아직 읽지 못했습니다"); + // what the assessment left open is still said + for (const reason of OPEN_KO) expect(html).toContain(reason); +}); + const usageStatus = (total: number | null, part: number | null) => ({ request: { operation_id: "op" }, completed_stages: [], result_usage: { op: { operation_id: "op", calls: 2, input_tokens: null, output_tokens: null, total_tokens: total, cached_input_tokens: null, diff --git a/apps/web/src/components/ResultHoldDetails.tsx b/apps/web/src/components/ResultHoldDetails.tsx index e28c9604..3621e5f6 100644 --- a/apps/web/src/components/ResultHoldDetails.tsx +++ b/apps/web/src/components/ResultHoldDetails.tsx @@ -20,9 +20,11 @@ import type { ResearchDetail } from "./ResearchResultCard"; * check, and one folded block of technical details for all of it. One folding block under the answer; the * header tags keep the status and the counts visible when it is folded, so they are not repeated inside. */ -export function ResultHoldDetails({ result, historySelection, coverageMatrix, progressSummary, nextUserAction, decisionDelta, onDetail }: { +export function ResultHoldDetails({ result, historySelection, coverageMatrix, progressSummary, nextUserAction, decisionDelta, earlierRound, onDetail }: { result: Record; historySelection?: HistorySelection; coverageMatrix?: CoverageMatrix | null; progressSummary?: UserProgressSummary | null; nextUserAction?: NextUserAction | null; decisionDelta?: DecisionDelta | null; + /** A saved round before the latest one: its answer checks are not sent, and never will be. */ + earlierRound?: boolean; onDetail: (detail: ResearchDetail) => void; }) { const language = useResultLanguage(); @@ -39,7 +41,7 @@ export function ResultHoldDetails({ result, historySelection, coverageMatrix, pr ]; // Open by default (the mock-up R1 shows it open); the header tags keep the counts visible once the reader folds it. return - {status && }/>} + {status && }/>} onDetail({ kind: "evidence", result, historySelection }) : undefined}/> {gaps.length > 0 && ( diff --git a/apps/web/src/components/Select.test.tsx b/apps/web/src/components/Select.test.tsx new file mode 100644 index 00000000..116fcf9f --- /dev/null +++ b/apps/web/src/components/Select.test.tsx @@ -0,0 +1,27 @@ +import { renderToStaticMarkup } from "react-dom/server"; +import { readdirSync, readFileSync } from "node:fs"; +import { join, resolve } from "node:path"; +import { expect, it } from "vitest"; +import { Select } from "./Select"; + +it("draws a select whose arrow has no English tooltip and is hidden from a screen reader", () => { + const html = renderToStaticMarkup(

{t("sourceTime.notice")}

{assessment.cutoff_state !== "PROHIBITED_CONTEXT" && <> diff --git a/apps/web/src/components/WorkspaceTopbar.tsx b/apps/web/src/components/WorkspaceTopbar.tsx index d21fcfcc..b2304e12 100644 --- a/apps/web/src/components/WorkspaceTopbar.tsx +++ b/apps/web/src/components/WorkspaceTopbar.tsx @@ -86,4 +86,3 @@ export function WorkspaceTopbar({ drawer, testMode, projectLabel, threadLabel, c ); } - diff --git a/apps/web/src/components/history/ResearchHistoryWorkspace.tsx b/apps/web/src/components/history/ResearchHistoryWorkspace.tsx index c031aa49..6aa2a837 100644 --- a/apps/web/src/components/history/ResearchHistoryWorkspace.tsx +++ b/apps/web/src/components/history/ResearchHistoryWorkspace.tsx @@ -8,7 +8,6 @@ import { selectionKey, type HistoryKind, type HistoryScope, type HistorySelectio import { HistoryDetail } from "./HistoryDetail"; import { HistoryTimeline } from "./HistoryTimeline"; import { Disclosure } from "../Disclosure"; -import { eyebrowLabel } from "../statusLabels"; import { ProjectReviewEmptyNote, ProjectReviewList } from "./ProjectReviewList"; import { DecisionDeltaComparison } from "./DecisionDeltaComparison"; import { useReadAccessProtection } from "../useReadAccessProtection"; @@ -118,7 +117,7 @@ export function ResearchHistoryWorkspace({ projectId, threadId, requestDigest, i return
{import.meta.env.VITE_THOTH_TEST_MODE === "true" && {t("historyWorkspace.testMode")}} {/* The panel has its own title ("변경 이력"); a second, different one inside it read as another feature. */} - {!compact &&

{eyebrowLabel("RESEARCH HISTORY")}

{kinds?.length === 1 && kinds[0] === "MEMORY" ? t("result.memory") : t("workspace.history")}

{t("historyWorkspace.preserve")}

+ {!compact &&

{kinds?.length === 1 && kinds[0] === "MEMORY" ? t("result.memory") : t("workspace.history")}

{t("historyWorkspace.preserve")}

{!initialSelection && !requestDigest && threadId &&
}
} {compact && initialSelection diff --git a/apps/web/src/components/statusLabels.test.tsx b/apps/web/src/components/statusLabels.test.tsx index 18433e6d..becf2670 100644 --- a/apps/web/src/components/statusLabels.test.tsx +++ b/apps/web/src/components/statusLabels.test.tsx @@ -31,7 +31,7 @@ it("names answer status, provider routes and English eyebrows", () => { expect(answerStatusLabel(undefined)).toBeNull(); expect(providerLabel("codex-oauth")).toBe("ChatGPT 계정"); expect(providerLabel("mystery-route")).toBe("mystery-route"); - expect(eyebrowLabel("EVIDENCE")).toBe("근거"); + expect(eyebrowLabel("LOCAL WORKSPACE")).toBe("이 컴퓨터"); expect(eyebrowLabel("SOMETHING ELSE")).toBe("SOMETHING ELSE"); expect(reversibilityLabel("PARTIAL")).toBe("일부만 되돌릴 수 있음"); expect(actionStateLabel("APPROVED")).toBe("승인됨"); diff --git a/apps/web/src/components/useNavigationDrawer.ts b/apps/web/src/components/useNavigationDrawer.ts index f893979e..21521ab7 100644 --- a/apps/web/src/components/useNavigationDrawer.ts +++ b/apps/web/src/components/useNavigationDrawer.ts @@ -60,4 +60,3 @@ export function useNavigationDrawer() { } export type NavigationDrawer = ReturnType; - diff --git a/apps/web/src/i18n/apiNoticesMessages.ts b/apps/web/src/i18n/apiNoticesMessages.ts index 6c287017..1e5c1eaf 100644 --- a/apps/web/src/i18n/apiNoticesMessages.ts +++ b/apps/web/src/i18n/apiNoticesMessages.ts @@ -115,6 +115,10 @@ export const apiNoticesMessages = { "ko": "답변 점검 항목 정보를 아직 읽지 못했습니다", "en": "Answer-check information is not available yet" }, + "answerHold.coverageEarlier": { + "ko": "이 회차의 답변 점검 항목은 여기서 볼 수 없습니다. 최신 회차의 답에서만 보입니다.", + "en": "The answer checks of this turn are not shown here. They are shown for the latest turn only." + }, "approvalView.approved": { "ko": "승인됨 · 이 내용으로 유효", "en": "Approved · valid for this content" diff --git a/apps/web/src/i18n/countFormatMessages.ts b/apps/web/src/i18n/countFormatMessages.ts index 72a94382..48655d22 100644 --- a/apps/web/src/i18n/countFormatMessages.ts +++ b/apps/web/src/i18n/countFormatMessages.ts @@ -27,7 +27,7 @@ export const countFormats = { otherItemsChanged:{ko:(n:number)=>`그 밖의 항목 ${n}개가 바뀌었습니다.`,en:(n:number)=>`${n} other ${n===1?"item changed":"items changed"}.`, "zh-Hans": (n:number)=>`其他 ${n} 项发生了变化。`, "zh-Hant": (n:number)=>`其他 ${n} 項發生了變化。`, ja: (n:number)=>`そのほかの項目 ${n} 件が変更されました。`}, observation:{ko:(text:string,refs:string)=>`관찰: ${text}${refs?` (근거 참조 ${refs})`:""}`,en:(text:string,refs:string)=>`Observation: ${text}${refs?` (evidence references: ${refs})`:""}`, "zh-Hans": (text:string,refs:string)=>`观察:${text}${refs?` (证据引用:${refs})`:""}`, "zh-Hant": (text:string,refs:string)=>`觀察:${text}${refs?` (證據引用:${refs})`:""}`, ja: (text:string,refs:string)=>`観察:${text}${refs?`(根拠の参照 ${refs})`:""}`}, pastTest:{ko:(state:string)=>`이전 시험 결과: ${state}`,en:(state:string)=>`Earlier test result: ${state}`, "zh-Hans": (state:string)=>`较早的测试结果:${state}`, "zh-Hant": (state:string)=>`較早的測試結果:${state}`, ja: (state:string)=>`以前のテスト結果:${state}`}, - pastExclusion:{ko:(state:string)=>`이전 조사에서 한 가설이 ${state}로 표시되었습니다`,en:(state:string)=>`A hypothesis in an earlier investigation was marked ${state}`, "zh-Hans": (state:string)=>`较早调查中的一个假设被标记为${state}`, "zh-Hant": (state:string)=>`較早調查中的一個假設被標記為${state}`, ja: (state:string)=>`以前の調査で、ある仮説が「${state}」と記録されました`}, + pastExclusion:{ko:(state:string)=>`이전 조사에서 한 가설의 표시: ${state}`,en:(state:string)=>`A hypothesis in an earlier investigation was marked ${state}`, "zh-Hans": (state:string)=>`较早调查中的一个假设被标记为${state}`, "zh-Hant": (state:string)=>`較早調查中的一個假設被標記為${state}`, ja: (state:string)=>`以前の調査で、ある仮説が「${state}」と記録されました`}, conditionScope:{ko:(label:string,scope:string)=>`${label}(조건 범위: ${scope})`,en:(label:string,scope:string)=>`${label} (condition scope: ${scope})`, "zh-Hans": (label:string,scope:string)=>`${label}(条件范围:${scope})`, "zh-Hant": (label:string,scope:string)=>`${label}(條件範圍:${scope})`, ja: (label:string,scope:string)=>`${label}(条件の範囲:${scope})`}, withheldLessons:{ko:(stale:number,refuted:number)=>`원본 기록은 남아 있습니다. 근거가 바뀌었거나 반대 결과가 나와 뺀 교훈은 보이지 않습니다(${[stale>0?"낡음 "+stale+"개":"",refuted>0?"반박됨 "+refuted+"개":""].filter(Boolean).join(" · ")}).`,en:(stale:number,refuted:number)=>`Original records are retained. Lessons excluded after evidence changes or contrary results are hidden (${[stale>0?stale+" stale":"",refuted>0?refuted+" refuted":""].filter(Boolean).join(" · ")}).`, "zh-Hans": (stale:number,refuted:number)=>`原始记录会保留。证据变化后或出现相反结果后被排除的经验教训不再显示(${[stale>0?stale+" 条过时":"",refuted>0?refuted+" 条被反驳":""].filter(Boolean).join(" · ")})。`, "zh-Hant": (stale:number,refuted:number)=>`原始記錄會保留。證據變化後或出現相反結果後被排除的經驗教訓不再顯示(${[stale>0?stale+" 條過時":"",refuted>0?refuted+" 條被反駁":""].filter(Boolean).join(" · ")})。`, ja: (stale:number,refuted:number)=>`元の記録は残っています。根拠が変わったり反対の結果が出たりして外された教訓は、表示されません(${[stale>0?"古い "+stale+" 件":"",refuted>0?"反証された "+refuted+" 件":""].filter(Boolean).join(" · ")})。`}, investigatedAt:{ko:(time:string)=>`${time} 조사`,en:(time:string)=>`Investigated ${time}`, "zh-Hans": (time:string)=>`${time} 调查`, "zh-Hant": (time:string)=>`${time} 調查`, ja: (time:string)=>`${time} の調査`}, diff --git a/apps/web/src/i18n/formatMessages.ts b/apps/web/src/i18n/formatMessages.ts index 8a4ae128..303bc8a9 100644 --- a/apps/web/src/i18n/formatMessages.ts +++ b/apps/web/src/i18n/formatMessages.ts @@ -29,7 +29,7 @@ const formats = { detachedSources: { ko: (count: number) => `연결을 해제한 자료 ${count}개`, en: (count: number) => `${count} disconnected ${count === 1 ? "source" : "sources"}`, "zh-Hans": (count: number) => `已断开连接的资料 ${count} 个`, "zh-Hant": (count: number) => `已中斷連線的資料 ${count} 個`, ja: (count: number) => `接続を解除した資料 ${count} 件` }, projectMenu: { ko: (name: string) => `${name} 프로젝트 메뉴`, en: (name: string) => `Project menu for ${name}`, "zh-Hans": (name: string)=>`${name} 的项目菜单`, "zh-Hant": (name: string)=>`${name} 的專案選單`, ja: (name: string)=>`${name} のプロジェクトメニュー` }, archiveProject: { - ko: (name: string) => `프로젝트 "${name}"을 보관할까요? 보관하면 읽기 전용이 되고 목록에서 숨겨집니다. 보관은 되돌릴 수 있습니다. '보관한 프로젝트 보기'에서 되살립니다.`, + ko: (name: string) => `"${name}" 프로젝트를 보관할까요? 보관하면 읽기 전용이 되고 목록에서 숨겨집니다. 보관은 되돌릴 수 있습니다. '보관한 프로젝트 보기'에서 되살립니다.`, en: (name: string) => `Archive project "${name}"? It becomes read-only and is hidden from the list. You can restore it from archived projects.`, "zh-Hans": (name: string)=>`要归档项目“${name}”吗?归档后它将变为只读,并从列表中隐藏。你可以在“已归档的项目”中将其恢复。`, "zh-Hant": (name: string)=>`要歸檔專案“${name}”嗎?歸檔後它將變為唯讀,並從列表中隱藏。你可以在“已歸檔的專案”中將其還原。`, ja: (name: string)=>`プロジェクト「${name}」をアーカイブしますか?アーカイブすると読み取り専用になり、一覧から非表示になります。「アーカイブ済みのプロジェクトを見る」から復元できます。`, }, cutoff: { ko: (value: string | null) => `기준시점 ${value}`, en: (value: string | null) => `Cutoff ${value}`, "zh-Hans": (value: string | null)=>`截止时间 ${value}`, "zh-Hant": (value: string | null)=>`截止時間 ${value}`, ja: (value: string | null)=>`基準時点 ${value}` }, diff --git a/apps/web/src/i18n/ja/apiNoticesMessagesJa.ts b/apps/web/src/i18n/ja/apiNoticesMessagesJa.ts index 5dc6af67..bce6e378 100644 --- a/apps/web/src/i18n/ja/apiNoticesMessagesJa.ts +++ b/apps/web/src/i18n/ja/apiNoticesMessagesJa.ts @@ -31,6 +31,7 @@ export const apiNoticesJa: ColumnOf = { "memorySwitch.label": "調査で記憶を使う", "answerHold.title": "確認された事実と根拠の用途", "answerHold.coverageMissing": "回答のチェック情報はまだありません", + "answerHold.coverageEarlier": "この回の回答チェック項目はここには表示されません。最新の回の回答にだけ表示されます。", "approvalView.approved": "承認済み · この内容に有効", "approvalView.pending": "承認待ち", "approvalView.rejected": "却下", diff --git a/apps/web/src/i18n/ja/sourceMessagesJa.ts b/apps/web/src/i18n/ja/sourceMessagesJa.ts index c787b6a7..69bb8173 100644 --- a/apps/web/src/i18n/ja/sourceMessagesJa.ts +++ b/apps/web/src/i18n/ja/sourceMessagesJa.ts @@ -6,7 +6,6 @@ export const sourceJa: ColumnOf = { "sources.fileUploadCancel": "アップロードをキャンセル", "sources.fileUploadCancelled": "アップロードをキャンセルしました。資料は接続されていません。", "sources.fileTitle": "資料ファイルを接続", - "sources.preserve": "元の文章、バージョン、位置を保持します", "sources.fileTypes": "HWPX、PDF、DOCX、XLSX、CSV、JSON、MD、TXT、YAML、HTML のファイルをアップロードし、その権威性と利用範囲を記録します。", "sources.hostedUpload": "アップロードしたファイルは、このレビュー用プロジェクトで使われます。", "sources.localUpload": "ファイルはこのコンピューターの THOTH ワークスペースに一時的に置かれます。", diff --git a/apps/web/src/i18n/ja/workspaceDetailMessagesJa.ts b/apps/web/src/i18n/ja/workspaceDetailMessagesJa.ts index e18e0f9c..9a07dedc 100644 --- a/apps/web/src/i18n/ja/workspaceDetailMessagesJa.ts +++ b/apps/web/src/i18n/ja/workspaceDetailMessagesJa.ts @@ -2,7 +2,6 @@ import type { ColumnOf } from "../withColumns"; import type { workspaceDetailMessages } from "../workspaceDetailMessages"; export const workspaceDetailJa: ColumnOf = { - "workspace.continueHeading": "調査を続けるか、新しい問題を始めてください。", "workspace.authorizedRead": "アクセス権限の範囲内で、保存済みの調査を引き続き読めます。", "workspace.readOnlyHold": "権限の範囲内で、保存済みの調査は引き続き利用できます。新しいモデルの実行と同意の変更は保留されています。", "workspace.resumeFailed": "調査を再開できませんでした。", diff --git a/apps/web/src/i18n/ja/workspaceMessagesJa.ts b/apps/web/src/i18n/ja/workspaceMessagesJa.ts index b8bfaa6c..6db2fc74 100644 --- a/apps/web/src/i18n/ja/workspaceMessagesJa.ts +++ b/apps/web/src/i18n/ja/workspaceMessagesJa.ts @@ -72,8 +72,6 @@ export const workspaceJa: ColumnOf = { "workspace.contextCorrupt": "最後のワークスペースの場所を読み込めませんでした。元の記録は削除されていません。", "workspace.contextUnavailable": "このブラウザから最後のワークスペースの場所を読み込めませんでした。ストレージへのアクセスとワークスペースの識別子を確認してください。", "workspace.backProjects": "プロジェクトの選択に戻る", - "workspace.openOrCreate": "既存のプロジェクトを開いて続けるか、新しい調査の文脈を作成してください。", - "workspace.continueProject": "続けるプロジェクト", "workspace.projects": "プロジェクト", "workspace.loadingProjects": "プロジェクトを読み込んでいます…", "workspace.openProject": "プロジェクトを開く", diff --git a/apps/web/src/i18n/sourceMessages.ts b/apps/web/src/i18n/sourceMessages.ts index 2c774c03..1ee6923f 100644 --- a/apps/web/src/i18n/sourceMessages.ts +++ b/apps/web/src/i18n/sourceMessages.ts @@ -12,10 +12,6 @@ export const sourceMessages = { "ko": "파일 자료 연결", "en": "Connect source files" }, - "sources.preserve": { - "ko": "원문 · 버전 · 위치 보존", - "en": "Preserve original text, versions, and locations" - }, "sources.fileTypes": { "ko": "HWPX · PDF · DOCX · XLSX · CSV · JSON · MD · TXT · YAML · HTML 파일을 올리고, 자료의 성격과 사용 범위를 함께 기록합니다.", "en": "Upload HWPX, PDF, DOCX, XLSX, CSV, JSON, MD, TXT, YAML, or HTML files and record their authority and usage scope." diff --git a/apps/web/src/i18n/statusChinese.ts b/apps/web/src/i18n/statusChinese.ts index e7327419..edba441d 100644 --- a/apps/web/src/i18n/statusChinese.ts +++ b/apps/web/src/i18n/statusChinese.ts @@ -25,9 +25,7 @@ export const statusChinese = { answerStatus: { PARTIAL_HOLD: "评估暂缓", ASSESSED_WITH_OPEN_CHECKS: "部分回答", ASSESSED_FOR_REQUEST: "回答完成" }, providers: { default: "默认连接", "codex-oauth": "ChatGPT 账户", "xai-oauth": "xAI 账户", "claude-oauth": "Claude 账户", "claude-code": "Claude Code 账户", openai: "OpenAI API 密钥", anthropic: "Anthropic API 密钥", xai: "xAI API 密钥" }, - eyebrows: { EVIDENCE: "证据", "AUTHORIZED SOURCES": "授权的资料", "RESEARCH HISTORY": "研究历史", - "RESEARCH WORKSPACE": "研究工作区", "LOCAL WORKSPACE": "这台电脑", "REVIEW WORKSPACE": "审查工作区", - "CAPABILITY WORKBENCH": "功能工作台", "CANONICAL RECORDS": "已保存的记录" }, + eyebrows: { "LOCAL WORKSPACE": "这台电脑", "REVIEW WORKSPACE": "审查工作区" }, reversibility: { FULL: "可逆", PARTIAL: "部分可逆", NONE: "不可逆" }, actionState: { APPROVED: "已批准", PARTIAL: "部分已执行", SUCCEEDED: "完成", FAILED: "失败", CANCELLED: "已取消" }, coverageTerms: { RESEARCH_GAP: "研究缺口", APPLIED: "已应用于评估", NOT_ASSESSED: "未评估", INCONCLUSIVE: "尚无定论", diff --git a/apps/web/src/i18n/statusEnglish.ts b/apps/web/src/i18n/statusEnglish.ts index c0e67e6b..a5d09fd3 100644 --- a/apps/web/src/i18n/statusEnglish.ts +++ b/apps/web/src/i18n/statusEnglish.ts @@ -25,9 +25,7 @@ export const statusEnglish = { answerStatus: { PARTIAL_HOLD: "Assessment on hold", ASSESSED_WITH_OPEN_CHECKS: "Partial answer", ASSESSED_FOR_REQUEST: "Answer complete" }, providers: { default: "Default connection", "codex-oauth": "ChatGPT account", "xai-oauth": "xAI account", "claude-oauth": "Claude account", "claude-code": "Claude Code account", openai: "OpenAI API key", anthropic: "Anthropic API key", xai: "xAI API key" }, - eyebrows: { EVIDENCE: "Evidence", "AUTHORIZED SOURCES": "Authorized sources", "RESEARCH HISTORY": "Research history", - "RESEARCH WORKSPACE": "Research workspace", "LOCAL WORKSPACE": "This computer", "REVIEW WORKSPACE": "Review workspace", - "CAPABILITY WORKBENCH": "Capability workbench", "CANONICAL RECORDS": "Saved records" }, + eyebrows: { "LOCAL WORKSPACE": "This computer", "REVIEW WORKSPACE": "Review workspace" }, reversibility: { FULL: "Reversible", PARTIAL: "Partially reversible", NONE: "Irreversible" }, actionState: { APPROVED: "Approved", PARTIAL: "Partially executed", SUCCEEDED: "Complete", FAILED: "Failed", CANCELLED: "Cancelled" }, coverageTerms: { RESEARCH_GAP: "Research gap", APPLIED: "Applied to the assessment", NOT_ASSESSED: "Not assessed", INCONCLUSIVE: "Inconclusive", diff --git a/apps/web/src/i18n/statusJapanese.ts b/apps/web/src/i18n/statusJapanese.ts index 3b5ad173..1564da64 100644 --- a/apps/web/src/i18n/statusJapanese.ts +++ b/apps/web/src/i18n/statusJapanese.ts @@ -25,9 +25,7 @@ export const statusJapanese = { answerStatus: { PARTIAL_HOLD: "判定は保留", ASSESSED_WITH_OPEN_CHECKS: "部分的な回答", ASSESSED_FOR_REQUEST: "回答完了" }, providers: { default: "既定の接続", "codex-oauth": "ChatGPT アカウント", "xai-oauth": "xAI アカウント", "claude-oauth": "Claude アカウント", "claude-code": "Claude Code アカウント", openai: "OpenAI API キー", anthropic: "Anthropic API キー", xai: "xAI API キー" }, - eyebrows: { EVIDENCE: "根拠", "AUTHORIZED SOURCES": "許可された資料", "RESEARCH HISTORY": "調査履歴", - "RESEARCH WORKSPACE": "調査ワークスペース", "LOCAL WORKSPACE": "このコンピューター", "REVIEW WORKSPACE": "レビュー用ワークスペース", - "CAPABILITY WORKBENCH": "機能ワークベンチ", "CANONICAL RECORDS": "保存された記録" }, + eyebrows: { "LOCAL WORKSPACE": "このコンピューター", "REVIEW WORKSPACE": "レビュー用ワークスペース" }, reversibility: { FULL: "元に戻せる", PARTIAL: "一部は元に戻せる", NONE: "元に戻せない" }, actionState: { APPROVED: "承認済み", PARTIAL: "一部実行済み", SUCCEEDED: "完了", FAILED: "失敗", CANCELLED: "キャンセル済み" }, coverageTerms: { RESEARCH_GAP: "調査の不足", APPLIED: "判定に反映済み", NOT_ASSESSED: "未評価", INCONCLUSIVE: "結論は出ていない", diff --git a/apps/web/src/i18n/statusResources.ts b/apps/web/src/i18n/statusResources.ts index 4d74b6b2..0c88adbf 100644 --- a/apps/web/src/i18n/statusResources.ts +++ b/apps/web/src/i18n/statusResources.ts @@ -53,9 +53,7 @@ providers: { "claude-code": "Claude Code 계정", openai: "OpenAI API 키", anthropic: "Anthropic API 키", xai: "xAI API 키", }, eyebrows: { - EVIDENCE: "근거", "AUTHORIZED SOURCES": "허용된 자료", "RESEARCH HISTORY": "연구 이력", - "RESEARCH WORKSPACE": "연구 작업 공간", "LOCAL WORKSPACE": "이 컴퓨터", "REVIEW WORKSPACE": "심사 작업 공간", - "CAPABILITY WORKBENCH": "기능 탐색", "CANONICAL RECORDS": "저장된 기록", + "LOCAL WORKSPACE": "이 컴퓨터", "REVIEW WORKSPACE": "심사 작업 공간", }, reversibility: { FULL: "되돌릴 수 있음", PARTIAL: "일부만 되돌릴 수 있음", NONE: "되돌릴 수 없음" }, actionState: { APPROVED: "승인됨", PARTIAL: "일부만 실행됨", SUCCEEDED: "완료", FAILED: "실패", CANCELLED: "취소됨" }, diff --git a/apps/web/src/i18n/statusTraditional.ts b/apps/web/src/i18n/statusTraditional.ts index 624e1895..4a323965 100644 --- a/apps/web/src/i18n/statusTraditional.ts +++ b/apps/web/src/i18n/statusTraditional.ts @@ -25,9 +25,7 @@ export const statusTraditional = { answerStatus: { PARTIAL_HOLD: "評估暫緩", ASSESSED_WITH_OPEN_CHECKS: "部分回答", ASSESSED_FOR_REQUEST: "回答完成" }, providers: { default: "預設連線", "codex-oauth": "ChatGPT 帳戶", "xai-oauth": "xAI 帳戶", "claude-oauth": "Claude 帳戶", "claude-code": "Claude Code 帳戶", openai: "OpenAI API 金鑰", anthropic: "Anthropic API 金鑰", xai: "xAI API 金鑰" }, - eyebrows: { EVIDENCE: "證據", "AUTHORIZED SOURCES": "授權的資料", "RESEARCH HISTORY": "研究歷史", - "RESEARCH WORKSPACE": "研究工作區", "LOCAL WORKSPACE": "這臺電腦", "REVIEW WORKSPACE": "審查工作區", - "CAPABILITY WORKBENCH": "功能工作臺", "CANONICAL RECORDS": "已儲存的記錄" }, + eyebrows: { "LOCAL WORKSPACE": "這臺電腦", "REVIEW WORKSPACE": "審查工作區" }, reversibility: { FULL: "可逆", PARTIAL: "部分可逆", NONE: "不可逆" }, actionState: { APPROVED: "已批准", PARTIAL: "部分已執行", SUCCEEDED: "完成", FAILED: "失敗", CANCELLED: "已取消" }, coverageTerms: { RESEARCH_GAP: "研究缺口", APPLIED: "已應用於評估", NOT_ASSESSED: "未評估", INCONCLUSIVE: "尚無定論", diff --git a/apps/web/src/i18n/wordingRules.test.ts b/apps/web/src/i18n/wordingRules.test.ts index af8202f5..e3c7ca3a 100644 --- a/apps/web/src/i18n/wordingRules.test.ts +++ b/apps/web/src/i18n/wordingRules.test.ts @@ -46,6 +46,26 @@ it("calls the kind of a source by one Korean word in the status labels too", () expect(labels).not.toContain("권위"); }); +// 을/를, 이/가, 은/는, 와/과, 로/으로 depend on the last sound of the word before them. A sentence that puts one +// straight after an inserted name or label is wrong for half of the names ('"2차"을 보관할까요'). +const PARTICLE_AFTER_VALUE = /\$\{[^}]+\}["'”’]?(?:을|를|이|가|은|는|와|과|으로|로)(?=[ ,.?!`])/; +// The login button names one of three accounts (ChatGPT, Claude, xAI); read in Korean all three end in a vowel +// sound, so "로" is right for each. A fourth account whose name ends in a consonant needs "으로" and a new sentence. +const PARTICLE_IS_RIGHT_FOR_EVERY_VALUE = new Set(["loginButton"]); + +it("puts no Korean particle straight after an inserted value", () => { + const found: string[] = []; + for (const [key, value] of Object.entries(formatCatalog)) { + const source = (value.ko as (...values: never[]) => string).toString(); + if (!PARTICLE_IS_RIGHT_FOR_EVERY_VALUE.has(key) && PARTICLE_AFTER_VALUE.test(source.slice(source.indexOf("=>") + 2))) found.push(key); + } + expect(found).toEqual([]); + for (const account of ["ChatGPT", "Claude", "xAI"]) expect(message("loginButton", account, false)).toBe(account + "로 로그인"); + // the check sees what it is meant to see + expect(PARTICLE_AFTER_VALUE.test('`프로젝트 "${name}"을 보관할까요?`')).toBe(true); + expect(PARTICLE_AFTER_VALUE.test('`"${name}" 프로젝트를 보관할까요?`')).toBe(false); +}); + // Text between tags that is written in capitals or as a bare English word is not in the translation // lists, so every language would see it in English. The product name is the one that stays. const FIXED_ENGLISH = />\s*([A-Z][A-Za-z]+(?: [A-Za-z]+)*)\s* { expect(found).toEqual([]); }); +// A small label above a heading ("eyebrow") is used only where it says something the heading under it does not. +// One above every heading, repeating the heading or the open tab, is decoration (the audit's slop list, 2). +const SMALL_LABELS: Record = { + "FirstRunSetup.tsx": "the step of the first-run setup (1 / 2), and that an existing setup needs a look", + "ModelCredentialPanel.tsx": "what the list of account names under it is for", + "ProjectSessionPages.tsx": "that the page belongs to the review site, which its title does not say", +}; + +it("puts a small label above a heading only where the label has something of its own to say", () => { + const used = new Map(); + for (const path of screenSources(resolve(import.meta.dirname, ".."))) { + const count = readFileSync(path, "utf8").split('className="eyebrow"').length - 1; + if (count > 0) used.set(path.split(/[\\/]/).pop()!, count); + } + expect([...used.keys()].sort()).toEqual(Object.keys(SMALL_LABELS).sort()); + expect(used.get("FirstRunSetup.tsx")).toBe(2); + expect(used.get("ModelCredentialPanel.tsx")).toBe(1); + expect(used.get("ProjectSessionPages.tsx")).toBe(1); +}); + it("names the workspace and the project list in every screen language", () => { for (const key of ["brand.tagline", "nav.projects", "nav.workspaceRoot"] as const) { const row = messages[key]; diff --git a/apps/web/src/i18n/workspaceDetailMessages.ts b/apps/web/src/i18n/workspaceDetailMessages.ts index 0438873b..100babdd 100644 --- a/apps/web/src/i18n/workspaceDetailMessages.ts +++ b/apps/web/src/i18n/workspaceDetailMessages.ts @@ -1,8 +1,4 @@ export const workspaceDetailMessages = { - "workspace.continueHeading": { - "ko": "연구를 이어가거나 새 문제를 시작하세요.", - "en": "Continue your research or start a new problem." - }, "workspace.authorizedRead": { "ko": "권한 있는 저장 연구는 계속 읽을 수 있습니다.", "en": "You can still read saved research within your access permissions." diff --git a/apps/web/src/i18n/workspaceMessages.ts b/apps/web/src/i18n/workspaceMessages.ts index 80b93f23..03b82e40 100644 --- a/apps/web/src/i18n/workspaceMessages.ts +++ b/apps/web/src/i18n/workspaceMessages.ts @@ -258,14 +258,6 @@ export const workspaceMessages = { "ko": "프로젝트 선택으로 돌아가기", "en": "Back to project selection" }, - "workspace.openOrCreate": { - "ko": "기존 프로젝트를 열어 작업을 이어가거나, 새로운 연구 맥락을 만들 수 있습니다.", - "en": "Open an existing project to continue, or create a new research context." - }, - "workspace.continueProject": { - "ko": "이어갈 프로젝트", - "en": "Project to continue" - }, "workspace.projects": { "ko": "프로젝트", "en": "Projects" diff --git a/apps/web/src/i18n/zh/apiNoticesMessagesZh.ts b/apps/web/src/i18n/zh/apiNoticesMessagesZh.ts index fb8a1536..1ce8d94f 100644 --- a/apps/web/src/i18n/zh/apiNoticesMessagesZh.ts +++ b/apps/web/src/i18n/zh/apiNoticesMessagesZh.ts @@ -31,6 +31,7 @@ export const apiNoticesZh: ColumnOf = { "memorySwitch.label": "在研究中使用记忆", "answerHold.title": "已确认的事实与证据用途", "answerHold.coverageMissing": "暂时没有回答核查信息", + "answerHold.coverageEarlier": "这一轮的回答核查项目不在这里显示,只在最新一轮的回答中显示。", "approvalView.approved": "已批准 · 对此内容有效", "approvalView.pending": "等待批准", "approvalView.rejected": "已拒绝", diff --git a/apps/web/src/i18n/zh/sourceMessagesZh.ts b/apps/web/src/i18n/zh/sourceMessagesZh.ts index 0ff52bd6..8b3a2f5a 100644 --- a/apps/web/src/i18n/zh/sourceMessagesZh.ts +++ b/apps/web/src/i18n/zh/sourceMessagesZh.ts @@ -6,7 +6,6 @@ export const sourceZh: ColumnOf = { "sources.fileUploadCancel": "取消上传", "sources.fileUploadCancelled": "上传已取消。没有连接任何资料。", "sources.fileTitle": "连接资料文件", - "sources.preserve": "保留原文、版本和位置", "sources.fileTypes": "上传 HWPX、PDF、DOCX、XLSX、CSV、JSON、MD、TXT、YAML 或 HTML 文件,并记录其权威性和使用范围。", "sources.hostedUpload": "上传的文件用于此审查项目。", "sources.localUpload": "文件暂存在这台电脑的 THOTH 工作区中。", diff --git a/apps/web/src/i18n/zh/workspaceDetailMessagesZh.ts b/apps/web/src/i18n/zh/workspaceDetailMessagesZh.ts index 269d2a91..50430429 100644 --- a/apps/web/src/i18n/zh/workspaceDetailMessagesZh.ts +++ b/apps/web/src/i18n/zh/workspaceDetailMessagesZh.ts @@ -2,7 +2,6 @@ import type { ColumnOf } from "../withColumns"; import type { workspaceDetailMessages } from "../workspaceDetailMessages"; export const workspaceDetailZh: ColumnOf = { - "workspace.continueHeading": "继续你的研究,或开始一个新问题。", "workspace.authorizedRead": "在你的访问权限范围内,仍可阅读已保存的研究。", "workspace.readOnlyHold": "在你的权限范围内,已保存的研究仍可使用。新的模型运行和同意变更已暂缓。", "workspace.resumeFailed": "无法继续研究。", diff --git a/apps/web/src/i18n/zh/workspaceMessagesZh.ts b/apps/web/src/i18n/zh/workspaceMessagesZh.ts index b4fab945..3ae2f46c 100644 --- a/apps/web/src/i18n/zh/workspaceMessagesZh.ts +++ b/apps/web/src/i18n/zh/workspaceMessagesZh.ts @@ -72,8 +72,6 @@ export const workspaceZh: ColumnOf = { "workspace.contextCorrupt": "无法读取上一次的工作区位置。原始记录没有被删除。", "workspace.contextUnavailable": "无法在此浏览器中读取上一次的工作区位置。请检查存储访问权限和工作区标识。", "workspace.backProjects": "返回项目选择", - "workspace.openOrCreate": "打开已有的项目以继续,或创建新的研究上下文。", - "workspace.continueProject": "要继续的项目", "workspace.projects": "项目", "workspace.loadingProjects": "正在加载项目…", "workspace.openProject": "打开项目", diff --git a/apps/web/src/i18n/zhHant/apiNoticesMessagesZhHant.ts b/apps/web/src/i18n/zhHant/apiNoticesMessagesZhHant.ts index ec57cc8a..484ce367 100644 --- a/apps/web/src/i18n/zhHant/apiNoticesMessagesZhHant.ts +++ b/apps/web/src/i18n/zhHant/apiNoticesMessagesZhHant.ts @@ -31,6 +31,7 @@ export const apiNoticesZhHant: ColumnOf = { "memorySwitch.label": "在研究中使用記憶", "answerHold.title": "已確認的事實與證據用途", "answerHold.coverageMissing": "暫時沒有回答核查資訊", + "answerHold.coverageEarlier": "這一輪的回答核查項目不在這裡顯示,只在最新一輪的回答中顯示。", "approvalView.approved": "已批准 · 對此內容有效", "approvalView.pending": "等待批准", "approvalView.rejected": "已拒絕", diff --git a/apps/web/src/i18n/zhHant/sourceMessagesZhHant.ts b/apps/web/src/i18n/zhHant/sourceMessagesZhHant.ts index e4808335..589ec8b8 100644 --- a/apps/web/src/i18n/zhHant/sourceMessagesZhHant.ts +++ b/apps/web/src/i18n/zhHant/sourceMessagesZhHant.ts @@ -6,7 +6,6 @@ export const sourceZhHant: ColumnOf = { "sources.fileUploadCancel": "取消上傳", "sources.fileUploadCancelled": "上傳已取消。沒有連線任何資料。", "sources.fileTitle": "連線資料檔案", - "sources.preserve": "保留原文、版本和位置", "sources.fileTypes": "上傳 HWPX、PDF、DOCX、XLSX、CSV、JSON、MD、TXT、YAML 或 HTML 檔案,並記錄其權威性和使用範圍。", "sources.hostedUpload": "上傳的檔案用於此審查專案。", "sources.localUpload": "檔案暫存在這臺電腦的 THOTH 工作區中。", diff --git a/apps/web/src/i18n/zhHant/workspaceDetailMessagesZhHant.ts b/apps/web/src/i18n/zhHant/workspaceDetailMessagesZhHant.ts index b0ae8cc5..017bab46 100644 --- a/apps/web/src/i18n/zhHant/workspaceDetailMessagesZhHant.ts +++ b/apps/web/src/i18n/zhHant/workspaceDetailMessagesZhHant.ts @@ -2,7 +2,6 @@ import type { ColumnOf } from "../withColumns"; import type { workspaceDetailMessages } from "../workspaceDetailMessages"; export const workspaceDetailZhHant: ColumnOf = { - "workspace.continueHeading": "繼續你的研究,或開始一個新問題。", "workspace.authorizedRead": "在你的存取權限範圍內,仍可閱讀已儲存的研究。", "workspace.readOnlyHold": "在你的權限範圍內,已儲存的研究仍可使用。新的模型執行和同意變更已暫緩。", "workspace.resumeFailed": "無法繼續研究。", diff --git a/apps/web/src/i18n/zhHant/workspaceMessagesZhHant.ts b/apps/web/src/i18n/zhHant/workspaceMessagesZhHant.ts index 46e6011d..091e4d1b 100644 --- a/apps/web/src/i18n/zhHant/workspaceMessagesZhHant.ts +++ b/apps/web/src/i18n/zhHant/workspaceMessagesZhHant.ts @@ -72,8 +72,6 @@ export const workspaceZhHant: ColumnOf = { "workspace.contextCorrupt": "無法讀取上一次的工作區位置。原始記錄沒有被刪除。", "workspace.contextUnavailable": "無法在此瀏覽器中讀取上一次的工作區位置。請檢查儲存存取權限和工作區標識。", "workspace.backProjects": "返回專案選擇", - "workspace.openOrCreate": "開啟已有的專案以繼續,或建立新的研究上下文。", - "workspace.continueProject": "要繼續的專案", "workspace.projects": "專案", "workspace.loadingProjects": "正在載入專案…", "workspace.openProject": "開啟專案", diff --git a/apps/web/src/narrowLayoutRules.test.ts b/apps/web/src/narrowLayoutRules.test.ts index eda018de..d51ba2ea 100644 --- a/apps/web/src/narrowLayoutRules.test.ts +++ b/apps/web/src/narrowLayoutRules.test.ts @@ -109,3 +109,13 @@ it("never shortens the state tag of a provider on the first-run screen", () => { expect(head[0].body).toContain("flex-wrap: wrap"); }); +it("never shortens the label of where this copy runs: it takes a row of its own when the name leaves no room", () => { + // English and Japanese do not fit next to the name in the project list's head; the label used to be cut to "THIS CO…". + const brand = all.filter(rule => has(rule, ".workstation .brand")); + expect(brand).toHaveLength(1); + expect(brand[0].body).toContain("flex-wrap: wrap"); + const tag = all.filter(rule => has(rule, ".workstation .brand .bp6-tag") && declares(rule, "flex")); + expect(tag.map(rule => rule.body.match(/flex:\s*([^;]+);/)?.[1])).toEqual(["none"]); + // nothing pushes the label off its row or gives it a width to be cut to + expect(all.filter(rule => has(rule, ".workstation .brand .bp6-tag") && (declares(rule, "max-width") || declares(rule, "width") || declares(rule, "overflow")))).toEqual([]); +}); diff --git a/apps/web/src/typeScaleRules.test.ts b/apps/web/src/typeScaleRules.test.ts index 13383d97..d62fd0dd 100644 --- a/apps/web/src/typeScaleRules.test.ts +++ b/apps/web/src/typeScaleRules.test.ts @@ -34,4 +34,3 @@ it("sets no font size in a component's own style", () => { const found = files(SOURCE, ".tsx").filter(path => /fontSize\s*:/.test(readFileSync(path, "utf8"))); expect(found).toEqual([]); }); - diff --git a/apps/web/src/workstation.css b/apps/web/src/workstation.css index 7f261792..c24672a3 100644 --- a/apps/web/src/workstation.css +++ b/apps/web/src/workstation.css @@ -76,11 +76,14 @@ button:focus-visible, summary:focus-visible, a:focus-visible { outline: 2px soli .workstation { display: grid; grid-template-columns: var(--sidebar-width) minmax(0,1fr); grid-template-rows: 56px 49px minmax(0,1fr); min-height: 100vh; } .workstation.with-evidence { grid-template-columns: var(--sidebar-width) minmax(450px,1fr) var(--evidence-width); } .workstation .navigation-rail { grid-column: 1; grid-row: 1 / -1; z-index: 10; background: #182029; border-right: 1px solid var(--line-soft); padding: 20px 12px 12px; height: 100vh; position: sticky; top: 0; gap: 12px; } -.workstation .brand { padding: 0 0 15px; gap: 9px; } +/* The name and the label of where this copy runs share a row when both fit. In a language where they do not + (English, Japanese), the label takes a row of its own and is shown whole; it is never shortened. */ +.workstation .brand { padding: 0 0 15px; gap: 6px 9px; flex-wrap: wrap; justify-content: space-between; } +.workstation .brand-name { display: flex; align-items: center; gap: 9px; min-width: 0; } .workstation .brand-mark { width: 34px; height: 34px; background: #294568; color: #9cc8ff; border: 1px solid #3d638b; border-radius: var(--radius); } .workstation .brand strong { font-size: 15px; letter-spacing: .18em; } .workstation .brand span { font-size: var(--font-small); letter-spacing: .09em; } -.workstation .brand .bp6-tag { margin-left: auto; font-size: var(--font-small); } +.workstation .brand .bp6-tag { flex: none; font-size: var(--font-small); } .workstation .nav-section { margin: 5px 0 0; } .workstation .nav-section-heading { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; } .workstation .nav-label { color: var(--faint); margin: 0 0 0 8px; font-size: var(--font-small); letter-spacing: .13em; } @@ -664,6 +667,9 @@ button:focus-visible, summary:focus-visible, a:focus-visible { outline: 2px soli /* ---- New project ---- */ .project-create { max-width: 760px; } .start-page.project-create h1 { margin: 8px 0 6px; font-size: 27px; line-height: 1.3; } +/* The home screen starts with the list of projects under a plain title of the same size. */ +.start-page.project-home h1 { margin: 0; font-size: 27px; line-height: 1.3; } +.start-page.project-home .project-home-list { margin-top: 0; } .project-create > .muted { margin: 0 0 18px; font-size: 13px; } .project-create .onboarding-card > .bp6-button { margin-top: 4px; }