From a9f354232acb55d1da67f79ff61beca2d07b4c84 Mon Sep 17 00:00:00 2001 From: 732642856 <732642856@qq.com> Date: Sat, 18 Jul 2026 04:17:08 +0800 Subject: [PATCH 1/8] feat: back daily starlanguage with API card --- .../src/app/api/daily-starlanguage/route.ts | 35 +++++++++++++++++ frontend/src/app/page.tsx | 38 ++++++++++++++++++- ...est_daily_and_rectification_entrypoints.py | 10 +++++ 3 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 frontend/src/app/api/daily-starlanguage/route.ts diff --git a/frontend/src/app/api/daily-starlanguage/route.ts b/frontend/src/app/api/daily-starlanguage/route.ts new file mode 100644 index 00000000..1a9394eb --- /dev/null +++ b/frontend/src/app/api/daily-starlanguage/route.ts @@ -0,0 +1,35 @@ +import { NextResponse } from "next/server"; + +type Profile = { + name?: string; + date?: string; + time?: string; + provinceCode?: string; + cityCode?: string; +}; + +const cards = [ + { trend: "先收束,再推进。适合把一个悬而未决的问题拆小。", action: "选一件最重要的事,给它留出 45 分钟不被打断的时间。", caution: "避免在情绪最满时做承诺。" }, + { trend: "适合整理关系与边界。越清楚,越不容易被外界节奏带走。", action: "把今天要回复的人和要推迟的事分开列出来。", caution: "不要把暂时的沉默误读成最终答案。" }, + { trend: "执行力比灵感更重要。小步完成会比大计划更有力量。", action: "先完成一个可交付版本,再考虑优化。", caution: "别让完美感拖慢开始。" }, + { trend: "适合观察资源流向:时间、注意力、金钱都算。", action: "检查一个正在消耗你的习惯,并给它设上限。", caution: "不要为了短期安心做长期成本高的选择。" }, +]; + +function pickCard(profile: Profile, today: string) { + const seed = `${today}-${profile.date ?? ""}-${profile.time ?? ""}-${profile.provinceCode ?? ""}-${profile.cityCode ?? ""}`; + const index = Array.from(seed).reduce((sum, char) => sum + char.charCodeAt(0), 0) % cards.length; + return cards[index]; +} + +export async function POST(request: Request) { + const body = await request.json().catch(() => null) as { profile?: Profile; today?: string } | null; + const profile = body?.profile ?? {}; + const today = body?.today || new Date().toISOString().slice(0, 10); + return NextResponse.json({ + status: "ok", + card: pickCard(profile, today), + source: "calculation_lite", + claim_status: "exploratory_unvalidated", + boundary: "not_deterministic_prediction", + }); +} diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 749990d0..09236803 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -74,6 +74,13 @@ type OnboardingContent = { greeting: string; suggestions: OnboardingSuggestion[] type OnboardingStep = "name" | "birth" | "place"; type GreetingPeriod = "morning" | "noon" | "afternoon" | "evening" | "late-night"; type DailyStarlanguageCard = { trend: string; action: string; caution: string }; +type DailyStarlanguageApiResponse = { + status?: "ok"; + card?: DailyStarlanguageCard; + source?: "calculation_lite"; + claim_status?: "exploratory_unvalidated"; + boundary?: "not_deterministic_prediction"; +}; type SessionReadResult = { readonly sessions: ChatSession[]; readonly fallbackSessionIds: string[] }; type PendingConsultation = { readonly requestId: string; @@ -341,6 +348,18 @@ function buildDailyStarlanguageCard(profile: Profile) { return dailyStarlanguageCards[index]; } +async function fetchDailyStarlanguage(profile: Profile) { + const response = await fetch("/api/daily-starlanguage", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ profile }), + }); + if (!response.ok) throw new Error("daily_starlanguage_unavailable"); + const payload = await response.json().catch(() => null) as DailyStarlanguageApiResponse | null; + if (payload?.status !== "ok" || !payload.card) throw new Error("daily_starlanguage_invalid"); + return payload.card; +} + function buildBirthTimeRectificationQuestion(profile: Profile) { return [ `请为${profile.name || "我"}做生时校正辅助。`, @@ -617,6 +636,7 @@ export default function Home() { const [otherProfileDraft, setOtherProfileDraft] = useState(emptyProfile); const [synastryReportCard, setSynastryReportCard] = useState(null); const [synastryHistory, setSynastryHistory] = useState([]); + const [dailyStarlanguageCard, setDailyStarlanguageCard] = useState(null); const [profileOpen, setProfileOpen] = useState(false); const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); const [profileNotice, setProfileNotice] = useState(""); @@ -732,7 +752,7 @@ export default function Home() { }, [accountId, profile]); const profileComplete = isProfileComplete(profile); - const dailyStarlanguage = profileComplete ? buildDailyStarlanguageCard(profile) : null; + const dailyStarlanguage = dailyStarlanguageCard ?? (profileComplete ? buildDailyStarlanguageCard(profile) : null); const onboardingPending = profileComplete && !onboarding && !onboardingError; const currentOnboardingMessage = onboardingJustCompleted ? startGreeting || completedOnboardingMessage(profileDraft.name.trim()) @@ -967,6 +987,22 @@ export default function Home() { }; }, [accountId, hydrated, onboarding, onboardingError, profile.name, profileComplete, startGreeting]); + useEffect(() => { + if (!hydrated || !profileComplete) return; + let cancelled = false; + setDailyStarlanguageCard(null); + void fetchDailyStarlanguage(profile) + .then((card) => { + if (!cancelled) setDailyStarlanguageCard(card); + }) + .catch(() => { + if (!cancelled) setDailyStarlanguageCard(buildDailyStarlanguageCard(profile)); + }); + return () => { + cancelled = true; + }; + }, [hydrated, profile.date, profile.time, profile.provinceCode, profile.cityCode, profileComplete]); + useEffect(() => { const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; conversationEnd.current?.scrollIntoView({ behavior: isLoading || reduceMotion ? "auto" : "smooth", block: "end" }); diff --git a/tests/test_daily_and_rectification_entrypoints.py b/tests/test_daily_and_rectification_entrypoints.py index e2357789..7c2613b9 100644 --- a/tests/test_daily_and_rectification_entrypoints.py +++ b/tests/test_daily_and_rectification_entrypoints.py @@ -2,11 +2,13 @@ from pathlib import Path PAGE = Path("frontend/src/app/page.tsx") +DAILY_ROUTE = Path("frontend/src/app/api/daily-starlanguage/route.ts") def test_daily_starlanguage_entrypoint_is_productized() -> None: source = PAGE.read_text(encoding="utf-8") assert "今日星语" in source + assert "fetchDailyStarlanguage" in source assert "buildDailyStarlanguageCard" in source assert "daily-starlanguage-card" in source assert "今日趋势" in source @@ -17,6 +19,14 @@ def test_daily_starlanguage_entrypoint_is_productized() -> None: assert "不是确定预测" in source +def test_daily_starlanguage_api_declares_honest_source_boundary() -> None: + source = DAILY_ROUTE.read_text(encoding="utf-8") + assert "status: \"ok\"" in source + assert "calculation_lite" in source + assert "exploratory_unvalidated" in source + assert "not_deterministic_prediction" in source + + def test_birth_time_rectification_entrypoint_is_productized() -> None: source = PAGE.read_text(encoding="utf-8") assert "生时校正" in source From 3ef595ebc91b0f3202d9df4c9f0f2420bdbece5f Mon Sep 17 00:00:00 2001 From: 732642856 <732642856@qq.com> Date: Sat, 18 Jul 2026 04:23:38 +0800 Subject: [PATCH 2/8] feat: enrich daily starlanguage with transit probe --- .../src/app/api/daily-starlanguage/route.ts | 84 ++++++++++++++++++- ...est_daily_and_rectification_entrypoints.py | 3 + 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/api/daily-starlanguage/route.ts b/frontend/src/app/api/daily-starlanguage/route.ts index 1a9394eb..c3303ed9 100644 --- a/frontend/src/app/api/daily-starlanguage/route.ts +++ b/frontend/src/app/api/daily-starlanguage/route.ts @@ -1,4 +1,5 @@ import { NextResponse } from "next/server"; +import { chinaLocations } from "@/data/china-locations"; type Profile = { name?: string; @@ -6,8 +7,11 @@ type Profile = { time?: string; provinceCode?: string; cityCode?: string; + districtCode?: string; }; +const jyotishApiBase = process.env.JYOTISH_API_BASE ?? "http://127.0.0.1:5200"; + const cards = [ { trend: "先收束,再推进。适合把一个悬而未决的问题拆小。", action: "选一件最重要的事,给它留出 45 分钟不被打断的时间。", caution: "避免在情绪最满时做承诺。" }, { trend: "适合整理关系与边界。越清楚,越不容易被外界节奏带走。", action: "把今天要回复的人和要推迟的事分开列出来。", caution: "不要把暂时的沉默误读成最终答案。" }, @@ -21,14 +25,90 @@ function pickCard(profile: Profile, today: string) { return cards[index]; } +function profilePayload(profile: Profile, today: string) { + if (!profile.date || !profile.time) return null; + const [year, month, day] = profile.date.split("-").map(Number); + const [hour, minute] = profile.time.split(":").map(Number); + const province = chinaLocations.country.provinces.find((item) => item.code === profile.provinceCode); + const city = province?.cities.find((item) => item.code === profile.cityCode); + const district = city?.districts.find((item) => item.code === profile.districtCode); + const location = district ?? city; + if (!year || !month || !day || Number.isNaN(hour) || Number.isNaN(minute) || !location) return null; + return { + year, + month, + day, + hour, + minute, + lat: location.center[1], + lon: location.center[0], + tz: chinaLocations.country.timezone, + transit_date: today, + today, + ayanamsa: "lahiri", + node_mode: "mean", + }; +} + +function chartPoints(chart: Record) { + const modules = chart.modules && typeof chart.modules === "object" ? chart.modules as Record : {}; + const chartModule = modules.chart && typeof modules.chart === "object" ? modules.chart as Record : chart; + const planets = chartModule.planets && typeof chartModule.planets === "object" ? chartModule.planets : undefined; + const ascendant = chartModule.ascendant && typeof chartModule.ascendant === "object" ? chartModule.ascendant : undefined; + return planets && ascendant ? { planets, ascendant } : null; +} + +async function fetchJson(path: string, body: Record) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 2500); + try { + const response = await fetch(`${jyotishApiBase}${path}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + cache: "no-store", + signal: controller.signal, + }); + if (!response.ok) throw new Error(`jyotish_api_${response.status}`); + return await response.json() as Record; + } finally { + clearTimeout(timeout); + } +} + +async function transitBackedCard(profile: Profile, today: string) { + const payload = profilePayload(profile, today); + if (!payload) return null; + const chart = await fetchJson("/api/chart", payload); + const points = chartPoints(chart); + if (!points) return null; + const tomorrow = new Date(`${today}T00:00:00.000Z`); + tomorrow.setUTCDate(tomorrow.getUTCDate() + 1); + const transit = await fetchJson("/api/transit", { + natal_planets: points.planets, + ascendant: points.ascendant, + start: today, + end: tomorrow.toISOString().slice(0, 10), + planets_to_check: ["Saturn", "Jupiter", "Rahu", "Ketu"], + }); + const summary = transit.summary && typeof transit.summary === "object" ? transit.summary as Record : {}; + const total = Number(summary.total_triggers ?? 0); + return { + trend: total > 0 ? `今日有 ${total} 个可观察过境触发点,适合把它当作时间窗口观察。` : "今日未发现强精确过境触发,适合按本命节奏稳步推进。", + action: "把今日计划压缩到一件主事,并记录实际发生的触发点。", + caution: "过境触发不能单独定事件,需与 Dasha、分盘和本命承诺交叉确认。", + }; +} + export async function POST(request: Request) { const body = await request.json().catch(() => null) as { profile?: Profile; today?: string } | null; const profile = body?.profile ?? {}; const today = body?.today || new Date().toISOString().slice(0, 10); + const transitCard = await transitBackedCard(profile, today).catch(() => null); return NextResponse.json({ status: "ok", - card: pickCard(profile, today), - source: "calculation_lite", + card: transitCard ?? pickCard(profile, today), + source: transitCard ? "jyotish_api_transit_lite" : "calculation_lite", claim_status: "exploratory_unvalidated", boundary: "not_deterministic_prediction", }); diff --git a/tests/test_daily_and_rectification_entrypoints.py b/tests/test_daily_and_rectification_entrypoints.py index 7c2613b9..6c3e27c7 100644 --- a/tests/test_daily_and_rectification_entrypoints.py +++ b/tests/test_daily_and_rectification_entrypoints.py @@ -22,6 +22,9 @@ def test_daily_starlanguage_entrypoint_is_productized() -> None: def test_daily_starlanguage_api_declares_honest_source_boundary() -> None: source = DAILY_ROUTE.read_text(encoding="utf-8") assert "status: \"ok\"" in source + assert "/api/chart" in source + assert "/api/transit" in source + assert "jyotish_api_transit_lite" in source assert "calculation_lite" in source assert "exploratory_unvalidated" in source assert "not_deterministic_prediction" in source From 7004a1bbbbd5acf2d354491c169fb52108f7bf42 Mon Sep 17 00:00:00 2001 From: 732642856 <732642856@qq.com> Date: Sat, 18 Jul 2026 04:28:56 +0800 Subject: [PATCH 3/8] feat: add birth rectification preview card --- .../src/app/api/birth-rectification/route.ts | 71 +++++++++++++++++++ frontend/src/app/globals.css | 10 +-- frontend/src/app/page.tsx | 50 +++++++++++-- ...est_daily_and_rectification_entrypoints.py | 11 +++ 4 files changed, 133 insertions(+), 9 deletions(-) create mode 100644 frontend/src/app/api/birth-rectification/route.ts diff --git a/frontend/src/app/api/birth-rectification/route.ts b/frontend/src/app/api/birth-rectification/route.ts new file mode 100644 index 00000000..9ac14b7b --- /dev/null +++ b/frontend/src/app/api/birth-rectification/route.ts @@ -0,0 +1,71 @@ +import { NextResponse } from "next/server"; +import { chinaLocations } from "@/data/china-locations"; + +type Profile = { + date?: string; + time?: string; + provinceCode?: string; + cityCode?: string; + districtCode?: string; +}; + +const jyotishApiBase = process.env.JYOTISH_API_BASE ?? "http://127.0.0.1:5200"; + +function payloadFromProfile(profile: Profile) { + if (!profile.date || !profile.time) return null; + const province = chinaLocations.country.provinces.find((item) => item.code === profile.provinceCode); + const city = province?.cities.find((item) => item.code === profile.cityCode); + const district = city?.districts.find((item) => item.code === profile.districtCode); + const location = district ?? city; + if (!location) return null; + return { + birth_time: `${profile.date} ${profile.time}`, + uncertainty_minutes: 30, + step_minutes: 2, + lat: location.center[1], + lon: location.center[0], + tz: chinaLocations.country.timezone, + }; +} + +async function fetchQuestionnaire(payload: Record) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 2500); + try { + const response = await fetch(`${jyotishApiBase}/api/active_rectification_questions`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + cache: "no-store", + signal: controller.signal, + }); + if (!response.ok) throw new Error(`jyotish_api_${response.status}`); + return await response.json() as Record; + } finally { + clearTimeout(timeout); + } +} + +export async function POST(request: Request) { + const body = await request.json().catch(() => null) as { profile?: Profile } | null; + const payload = payloadFromProfile(body?.profile ?? {}); + if (!payload) { + return NextResponse.json({ + status: "blocked", + boundary: "not_auto_rectified", + reason: "profile_incomplete", + }); + } + const questionnaire = await fetchQuestionnaire(payload).catch(() => null); + const candidateScan = questionnaire?.candidate_scan && typeof questionnaire.candidate_scan === "object" + ? questionnaire.candidate_scan + : null; + const questions = Array.isArray(questionnaire?.questions) ? questionnaire.questions : []; + return NextResponse.json({ + status: questionnaire ? "ok" : "blocked", + candidate_scan: candidateScan, + question_count: questions.length, + boundary: "not_auto_rectified", + source: questionnaire ? "active_rectification_questions" : "fallback_unavailable", + }); +} diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index b1152780..f8179149 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -315,14 +315,14 @@ button:disabled { cursor: default; opacity: .45; } .product-entrypoints button { min-height: 132px; align-items: end; border: 1px solid var(--color-border); background: var(--color-canvas); } .product-entrypoints span { display: grid; gap: var(--space-2); } .product-entrypoints small { color: var(--color-ink-secondary); line-height: 1.45; font-size: var(--type-caption); } -.daily-starlanguage-card { min-height: 132px; display: grid; gap: var(--space-3); padding: var(--space-5); border: 1px solid color-mix(in srgb, var(--color-action) 16%, var(--color-border)); border-radius: var(--radius-lg); background: var(--color-canvas); } +.daily-starlanguage-card, .birth-rectification-card { min-height: 132px; display: grid; gap: var(--space-3); padding: var(--space-5); border: 1px solid color-mix(in srgb, var(--color-action) 16%, var(--color-border)); border-radius: var(--radius-lg); background: var(--color-canvas); } .daily-starlanguage-heading { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); } .daily-starlanguage-heading > span { color: var(--color-action); font-size: var(--type-overline); font-weight: 600; letter-spacing: 1.5px; } .daily-starlanguage-heading button { min-height: 36px; display: inline-flex; align-items: center; gap: 6px; padding: 0 10px; color: var(--color-ink); font-size: var(--type-caption); } -.daily-starlanguage-card dl { display: grid; gap: var(--space-2); margin: 0; } -.daily-starlanguage-card div { display: grid; gap: 4px; } -.daily-starlanguage-card dt { color: var(--color-ink-secondary); font-size: var(--type-caption); } -.daily-starlanguage-card dd { margin: 0; color: var(--color-ink); line-height: 1.45; } +.daily-starlanguage-card dl, .birth-rectification-card dl { display: grid; gap: var(--space-2); margin: 0; } +.daily-starlanguage-card div, .birth-rectification-card div { display: grid; gap: 4px; } +.daily-starlanguage-card dt, .birth-rectification-card dt { color: var(--color-ink-secondary); font-size: var(--type-caption); } +.daily-starlanguage-card dd, .birth-rectification-card dd { margin: 0; color: var(--color-ink); line-height: 1.45; } .starter-loading { color: var(--color-ink-secondary); margin-left: 0; padding: var(--space-5); border-radius: var(--radius-lg); background: var(--color-canvas-muted); font-size: 14px; } .starter-note { margin: 10px 0 0; color: var(--color-ink-secondary); line-height: 1.5; grid-column: 1 / -1; font-size: 13px; } diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 09236803..bd90e1f1 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -81,6 +81,13 @@ type DailyStarlanguageApiResponse = { claim_status?: "exploratory_unvalidated"; boundary?: "not_deterministic_prediction"; }; +type BirthRectificationPreview = { + status?: "ok" | "blocked"; + candidate_scan?: { start?: string; end?: string; candidate_count?: number }; + question_count?: number; + boundary?: "not_auto_rectified"; + source?: "active_rectification_questions" | "fallback_unavailable"; +}; type SessionReadResult = { readonly sessions: ChatSession[]; readonly fallbackSessionIds: string[] }; type PendingConsultation = { readonly requestId: string; @@ -360,6 +367,16 @@ async function fetchDailyStarlanguage(profile: Profile) { return payload.card; } +async function fetchBirthRectificationPreview(profile: Profile) { + const response = await fetch("/api/birth-rectification", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ profile }), + }); + if (!response.ok) throw new Error("birth_rectification_preview_unavailable"); + return await response.json().catch(() => null) as BirthRectificationPreview | null; +} + function buildBirthTimeRectificationQuestion(profile: Profile) { return [ `请为${profile.name || "我"}做生时校正辅助。`, @@ -637,6 +654,7 @@ export default function Home() { const [synastryReportCard, setSynastryReportCard] = useState(null); const [synastryHistory, setSynastryHistory] = useState([]); const [dailyStarlanguageCard, setDailyStarlanguageCard] = useState(null); + const [birthRectificationPreview, setBirthRectificationPreview] = useState(null); const [profileOpen, setProfileOpen] = useState(false); const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); const [profileNotice, setProfileNotice] = useState(""); @@ -1003,6 +1021,22 @@ export default function Home() { }; }, [hydrated, profile.date, profile.time, profile.provinceCode, profile.cityCode, profileComplete]); + useEffect(() => { + if (!hydrated || !profileComplete) return; + let cancelled = false; + setBirthRectificationPreview(null); + void fetchBirthRectificationPreview(profile) + .then((preview) => { + if (!cancelled) setBirthRectificationPreview(preview); + }) + .catch(() => { + if (!cancelled) setBirthRectificationPreview({ status: "blocked", boundary: "not_auto_rectified", source: "fallback_unavailable" }); + }); + return () => { + cancelled = true; + }; + }, [hydrated, profile.date, profile.time, profile.provinceCode, profile.cityCode, profileComplete]); + useEffect(() => { const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; conversationEnd.current?.scrollIntoView({ behavior: isLoading || reduceMotion ? "auto" : "smooth", block: "end" }); @@ -2017,10 +2051,18 @@ export default function Home() { 探索性日提示,不是确定预测。 - +
+
+ 生时校正 + +
+
+
候选出生时间段
{birthRectificationPreview?.candidate_scan?.start && birthRectificationPreview?.candidate_scan?.end ? `${birthRectificationPreview.candidate_scan.start} – ${birthRectificationPreview.candidate_scan.end}` : "默认先扫描前后 30 分钟"}
+
候选点
{birthRectificationPreview?.candidate_scan?.candidate_count ? `${birthRectificationPreview.candidate_scan.candidate_count} 个` : "待后端生成"}
+
问题数
{birthRectificationPreview?.question_count ? `${birthRectificationPreview.question_count} 个事件问题` : "需补关键人生事件"}
+
+ 不能直接改写默认星盘;需事件证据验证。 +
))} diff --git a/tests/test_daily_and_rectification_entrypoints.py b/tests/test_daily_and_rectification_entrypoints.py index 6c3e27c7..40abc5f4 100644 --- a/tests/test_daily_and_rectification_entrypoints.py +++ b/tests/test_daily_and_rectification_entrypoints.py @@ -3,6 +3,7 @@ from pathlib import Path PAGE = Path("frontend/src/app/page.tsx") DAILY_ROUTE = Path("frontend/src/app/api/daily-starlanguage/route.ts") +RECTIFICATION_ROUTE = Path("frontend/src/app/api/birth-rectification/route.ts") def test_daily_starlanguage_entrypoint_is_productized() -> None: @@ -33,6 +34,16 @@ def test_daily_starlanguage_api_declares_honest_source_boundary() -> None: def test_birth_time_rectification_entrypoint_is_productized() -> None: source = PAGE.read_text(encoding="utf-8") assert "生时校正" in source + assert "fetchBirthRectificationPreview" in source + assert "birth-rectification-card" in source assert "draftBirthTimeRectificationQuestion" in source assert "候选出生时间段" in source assert "不能直接改写默认星盘" in source + + +def test_birth_rectification_api_proxies_active_questionnaire_with_boundary() -> None: + source = RECTIFICATION_ROUTE.read_text(encoding="utf-8") + assert "/api/active_rectification_questions" in source + assert "candidate_scan" in source + assert "question_count" in source + assert "not_auto_rectified" in source From ab9fb187f61510bb3903bd9ac886559fcc6fafd5 Mon Sep 17 00:00:00 2001 From: 732642856 <732642856@qq.com> Date: Sat, 18 Jul 2026 04:32:48 +0800 Subject: [PATCH 4/8] style: prioritize daily and rectification cards --- frontend/src/app/globals.css | 4 ++-- frontend/src/app/page.tsx | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index f8179149..3d375d17 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -303,7 +303,7 @@ button:disabled { cursor: default; opacity: .45; } .onboarding-card-heading small { color: var(--color-ink-secondary); line-height: 1.5; font-size: var(--type-caption); } .starter-list { border-top: 1px solid var(--color-border); display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-3); border: 0; } -.starter-list button { width: 100%; display: grid; align-items: end; border-bottom: 1px solid var(--color-border); cursor: pointer; text-align: left; transition: background-color 120ms ease-out, transform 120ms ease-out; min-height: 156px; grid-template-columns: minmax(0, 1fr) 20px; gap: var(--space-3); padding: var(--space-5); border: 1px solid transparent; border-radius: var(--radius-lg); background: var(--color-canvas-muted); } +.starter-list button { width: 100%; display: grid; align-items: end; border-bottom: 1px solid var(--color-border); cursor: pointer; text-align: left; transition: background-color 120ms ease-out, transform 120ms ease-out; min-height: 136px; grid-template-columns: minmax(0, 1fr) 20px; gap: var(--space-3); padding: var(--space-5); border: 1px solid transparent; border-radius: var(--radius-lg); background: var(--color-canvas-muted); } .starter-list button:first-child { border-color: color-mix(in srgb, var(--color-action) 18%, var(--color-border)); background: var(--color-action-soft); color: var(--color-ink); } .starter-list button:first-child .starter-content span { color: var(--color-ink); white-space: normal; } .starter-list button:first-child .starter-arrow { color: var(--color-action); } @@ -311,7 +311,7 @@ button:disabled { cursor: default; opacity: .45; } .starter-content b { color: var(--color-action); font-size: var(--type-overline); font-weight: 500; letter-spacing: 1.5px; } .starter-content span { overflow: hidden; color: var(--color-ink); font-family: var(--font-display); font-size: var(--type-title-md); line-height: 1.4; text-overflow: clip; text-wrap: pretty; white-space: normal; } .starter-arrow { width: 17px; height: 17px; color: var(--color-ink-secondary); } -.product-entrypoints { grid-column: 1 / -1; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-3); } +.product-entrypoints { grid-column: 1 / -1; display: grid; grid-template-columns: minmax(0, 1.12fr) minmax(0, .88fr); gap: var(--space-3); } .product-entrypoints button { min-height: 132px; align-items: end; border: 1px solid var(--color-border); background: var(--color-canvas); } .product-entrypoints span { display: grid; gap: var(--space-2); } .product-entrypoints small { color: var(--color-ink-secondary); line-height: 1.45; font-size: var(--type-caption); } diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index bd90e1f1..e8095d12 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -2028,16 +2028,6 @@ export default function Home() {
正在准备三个入门问题…
) : (
- {(onboarding?.suggestions ?? themes.map((item) => ({ theme: item.id, text: item.prompt }))).map((item) => { - const theme = themes.find((candidate) => candidate.id === item.theme); - return ( - - ); - })} - {onboardingError &&

Agent 的个性化入门问题暂时不可用,已显示安全的默认问题。

}
@@ -2064,6 +2054,16 @@ export default function Home() { 不能直接改写默认星盘;需事件证据验证。
+ {(onboarding?.suggestions ?? themes.map((item) => ({ theme: item.id, text: item.prompt }))).map((item) => { + const theme = themes.find((candidate) => candidate.id === item.theme); + return ( + + ); + })} + {onboardingError &&

Agent 的个性化入门问题暂时不可用,已显示安全的默认问题。

}
))}
From 1c46dd4e75a620fc92bcf3f0e8eb8176509be110 Mon Sep 17 00:00:00 2001 From: 732642856 <732642856@qq.com> Date: Sat, 18 Jul 2026 04:48:27 +0800 Subject: [PATCH 5/8] feat: add chat history management actions --- frontend/src/app/globals.css | 13 ++- frontend/src/app/page.tsx | 101 +++++++++++++++---- tests/test_session_management_entrypoints.py | 19 ++++ 3 files changed, 109 insertions(+), 24 deletions(-) create mode 100644 tests/test_session_management_entrypoints.py diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 3d375d17..b3c2bf51 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -264,11 +264,14 @@ button:disabled { cursor: default; opacity: .45; } .new-chat { width: 100%; min-height: 44px; display: flex; align-items: center; justify-content: center; gap: 8px; padding: 0 14px; border: 0; background: var(--color-surface-dark); color: var(--color-on-dark); cursor: pointer; font-size: var(--type-body-sm); transition: background-color 140ms ease-out, transform 120ms ease-out; margin: var(--space-4) 0 var(--space-6); border-radius: var(--radius-md); font-weight: 500; } .sidebar-label { display: block; padding: 0 var(--space-3) var(--space-2); color: var(--color-ink-tertiary); font-size: var(--type-overline); font-weight: 500; letter-spacing: 1.5px; } .session-list { min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: var(--space-1); } -.session-list button { position: relative; width: 100%; display: grid; gap: 2px; border: 0; background: transparent; cursor: pointer; text-align: left; transition: background-color 120ms ease-out, color 120ms ease-out, transform 120ms ease-out; min-height: 52px; padding: var(--space-2) var(--space-3) var(--space-2) var(--space-4); border-radius: var(--radius-md); color: var(--color-ink-secondary); } -.session-list button.is-active { color: var(--color-ink); background: var(--color-selected); } -.session-list button.is-active::before { position: absolute; border-radius: 3px; content: ""; top: var(--space-3); bottom: var(--space-3); left: var(--space-1); width: 2px; background: var(--color-action); } -.session-list button > span { overflow: hidden; line-height: 1.35; text-overflow: ellipsis; white-space: nowrap; font-size: 14px; font-weight: 500; } -.session-list button small { color: inherit; line-height: 1.3; opacity: .72; font-size: 12px; } +.session-row { position: relative; display: grid; gap: 4px; border-radius: var(--radius-md); color: var(--color-ink-secondary); } +.session-row > button { position: relative; width: 100%; display: grid; gap: 2px; border: 0; background: transparent; cursor: pointer; text-align: left; transition: background-color 120ms ease-out, color 120ms ease-out, transform 120ms ease-out; min-height: 52px; padding: var(--space-2) var(--space-3) var(--space-2) var(--space-4); border-radius: var(--radius-md); color: inherit; } +.session-row.is-active { color: var(--color-ink); background: var(--color-selected); } +.session-row.is-active::before { position: absolute; border-radius: 3px; content: ""; top: var(--space-3); bottom: var(--space-3); left: var(--space-1); width: 2px; background: var(--color-action); } +.session-row > button > span { overflow: hidden; line-height: 1.35; text-overflow: ellipsis; white-space: nowrap; font-size: 14px; font-weight: 500; } +.session-row > button small { color: inherit; line-height: 1.3; opacity: .72; font-size: 12px; } +.session-actions { display: flex; flex-wrap: wrap; gap: 4px; padding: 0 var(--space-2) var(--space-2) var(--space-4); } +.session-actions button { min-height: 26px; width: auto; padding: 0 7px; border: 1px solid var(--color-border); border-radius: 999px; background: var(--color-canvas); color: var(--color-ink-secondary); font-size: 11px; } .sidebar-footer { margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--color-border); } .profile-trigger { width: 100%; display: grid; grid-template-columns: 34px minmax(0, 1fr) 18px; align-items: center; gap: 9px; padding: 5px 7px; border: 0; background: transparent; color: var(--color-ink); cursor: pointer; text-align: left; transition: background-color 120ms ease-out, transform 120ms ease-out; min-height: 56px; border-radius: var(--radius-md); } .profile-initial { width: 32px; height: 32px; display: grid; place-items: center; border-radius: 50%; font-size: 12px; text-transform: uppercase; border: 0; background: var(--color-action); color: var(--color-on-dark); font-weight: 500; } diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index e8095d12..74ba4e51 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -667,6 +667,8 @@ export default function Home() { const [redeeming, setRedeeming] = useState(false); const [signingOut, setSigningOut] = useState(false); const [sessions, setSessions] = useState([]); + const [pinnedSessionIds, setPinnedSessionIds] = useState([]); + const [archivedSessionIds, setArchivedSessionIds] = useState([]); const [modelCatalog, setModelCatalog] = useState(null); const [activeSessionId, setActiveSessionId] = useState(""); const [draft, setDraft] = useState(""); @@ -710,16 +712,32 @@ export default function Home() { const uiPreviewMode = useRef(null); const activeSession = sessions.find((session) => session.id === activeSessionId) ?? sessions[0]; + const visibleSessions = sessions + .filter((session) => !archivedSessionIds.includes(session.id)) + .sort((left, right) => Number(pinnedSessionIds.includes(right.id)) - Number(pinnedSessionIds.includes(left.id))); const activeError = requestError && requestError.sessionId === activeSession?.id ? requestError.message : ""; const isLoading = pendingSessionId === activeSession?.id; const activeStreamingText = streamingReply && streamingReply.sessionId === activeSession?.id ? streamingReply.text : ""; + const accountId = account?.user.id; useEffect(() => { activeSessionIdRef.current = activeSessionId; }, [activeSessionId]); - const activeSuggestions = activeSession?.messages.reduce((latest, message) => message.role === "assistant" && message.suggestions?.length ? message.suggestions : latest, [] as string[]) ?? []; - const accountId = account?.user.id; + useEffect(() => { + if (!hydrated || !accountId) return; + const prefix = `jyotisha-session-controls:${accountId}:`; + setPinnedSessionIds(JSON.parse(localStorage.getItem(`${prefix}pinned`) || "[]")); + setArchivedSessionIds(JSON.parse(localStorage.getItem(`${prefix}archived`) || "[]")); + }, [accountId, hydrated]); + + useEffect(() => { + if (!hydrated || !accountId) return; + const prefix = `jyotisha-session-controls:${accountId}:`; + localStorage.setItem(`${prefix}pinned`, JSON.stringify(pinnedSessionIds)); + localStorage.setItem(`${prefix}archived`, JSON.stringify(archivedSessionIds)); + }, [accountId, archivedSessionIds, hydrated, pinnedSessionIds]); + const activeSuggestions = activeSession?.messages.reduce((latest, message) => message.role === "assistant" && message.suggestions?.length ? message.suggestions : latest, [] as string[]) ?? []; useEffect(() => { if (!accountId) { setChartLibrary([]); @@ -1121,6 +1139,45 @@ export default function Home() { if (insertError) throw new Error(`云端同步失败:${insertError.message}`); } + async function renameSession(session: ChatSession) { + const title = window.prompt("重命名聊天记录", session.title)?.trim(); + if (!title || title === session.title) return; + const nextSession = { ...session, title, updatedAt: timestamp() }; + updateSession(session.id, () => nextSession); + try { + await persistSession(nextSession); + } catch (caught) { + setComposerNotice(caught instanceof Error ? caught.message : "重命名同步失败"); + } + } + + async function deleteSession(session: ChatSession) { + if (!account || !window.confirm(`删除“${session.title}”?此操作不可恢复。`)) return; + const previousSessions = sessions; + const nextSessions = sessions.filter((item) => item.id !== session.id); + setSessions(nextSessions); + setPinnedSessionIds((current) => current.filter((id) => id !== session.id)); + setArchivedSessionIds((current) => current.filter((id) => id !== session.id)); + if (activeSessionId === session.id) setActiveSessionId(nextSessions[0]?.id ?? ""); + try { + const supabase = createBrowserSupabaseClient(); + const { error } = await supabase.from("chat_sessions").delete().eq("id", session.id).eq("user_id", account.user.id); + if (error) throw error; + } catch (caught) { + setSessions(previousSessions); + setComposerNotice(caught instanceof Error ? `删除失败:${caught.message}` : "删除失败"); + } + } + + function togglePinnedSession(sessionId: string) { + setPinnedSessionIds((current) => current.includes(sessionId) ? current.filter((id) => id !== sessionId) : [sessionId, ...current]); + } + + function toggleArchivedSession(sessionId: string) { + setArchivedSessionIds((current) => current.includes(sessionId) ? current.filter((id) => id !== sessionId) : [sessionId, ...current]); + if (activeSessionId === sessionId) setActiveSessionId(visibleSessions.find((session) => session.id !== sessionId)?.id ?? ""); + } + async function startNewChat() { if (!account || !modelCatalog || creatingSession) return; setMobileSidebarOpen(false); @@ -1934,23 +1991,29 @@ export default function Home() { diff --git a/tests/test_session_management_entrypoints.py b/tests/test_session_management_entrypoints.py new file mode 100644 index 00000000..580aa8ca --- /dev/null +++ b/tests/test_session_management_entrypoints.py @@ -0,0 +1,19 @@ +from pathlib import Path + + +PAGE = Path("frontend/src/app/page.tsx") + + +def test_chat_history_management_actions_are_exposed() -> None: + source = PAGE.read_text(encoding="utf-8") + for expected in ( + "renameSession", + "deleteSession", + "togglePinnedSession", + "toggleArchivedSession", + "置顶", + "重命名", + "归档", + "删除", + ): + assert expected in source From ee1bca0a7f6d06560bb6d7d91722b12ffdcc131c Mon Sep 17 00:00:00 2001 From: 732642856 <732642856@qq.com> Date: Sat, 18 Jul 2026 05:53:15 +0800 Subject: [PATCH 6/8] fix: move chat actions into context menu --- frontend/src/app/globals.css | 16 +++++--- frontend/src/app/page.tsx | 41 ++++++++++++++++---- tests/test_session_management_entrypoints.py | 5 +++ 3 files changed, 49 insertions(+), 13 deletions(-) diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index b3c2bf51..768e4632 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -264,14 +264,18 @@ button:disabled { cursor: default; opacity: .45; } .new-chat { width: 100%; min-height: 44px; display: flex; align-items: center; justify-content: center; gap: 8px; padding: 0 14px; border: 0; background: var(--color-surface-dark); color: var(--color-on-dark); cursor: pointer; font-size: var(--type-body-sm); transition: background-color 140ms ease-out, transform 120ms ease-out; margin: var(--space-4) 0 var(--space-6); border-radius: var(--radius-md); font-weight: 500; } .sidebar-label { display: block; padding: 0 var(--space-3) var(--space-2); color: var(--color-ink-tertiary); font-size: var(--type-overline); font-weight: 500; letter-spacing: 1.5px; } .session-list { min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: var(--space-1); } -.session-row { position: relative; display: grid; gap: 4px; border-radius: var(--radius-md); color: var(--color-ink-secondary); } -.session-row > button { position: relative; width: 100%; display: grid; gap: 2px; border: 0; background: transparent; cursor: pointer; text-align: left; transition: background-color 120ms ease-out, color 120ms ease-out, transform 120ms ease-out; min-height: 52px; padding: var(--space-2) var(--space-3) var(--space-2) var(--space-4); border-radius: var(--radius-md); color: inherit; } +.session-row { position: relative; display: grid; grid-template-columns: minmax(0, 1fr) 30px; align-items: center; border-radius: var(--radius-md); color: var(--color-ink-secondary); } +.session-row > button:first-child { position: relative; width: 100%; display: grid; gap: 2px; border: 0; background: transparent; cursor: pointer; text-align: left; transition: background-color 120ms ease-out, color 120ms ease-out, transform 120ms ease-out; min-height: 52px; padding: var(--space-2) var(--space-3) var(--space-2) var(--space-4); border-radius: var(--radius-md); color: inherit; } .session-row.is-active { color: var(--color-ink); background: var(--color-selected); } .session-row.is-active::before { position: absolute; border-radius: 3px; content: ""; top: var(--space-3); bottom: var(--space-3); left: var(--space-1); width: 2px; background: var(--color-action); } -.session-row > button > span { overflow: hidden; line-height: 1.35; text-overflow: ellipsis; white-space: nowrap; font-size: 14px; font-weight: 500; } -.session-row > button small { color: inherit; line-height: 1.3; opacity: .72; font-size: 12px; } -.session-actions { display: flex; flex-wrap: wrap; gap: 4px; padding: 0 var(--space-2) var(--space-2) var(--space-4); } -.session-actions button { min-height: 26px; width: auto; padding: 0 7px; border: 1px solid var(--color-border); border-radius: 999px; background: var(--color-canvas); color: var(--color-ink-secondary); font-size: 11px; } +.session-row > button:first-child > span { overflow: hidden; line-height: 1.35; text-overflow: ellipsis; white-space: nowrap; font-size: 14px; font-weight: 500; } +.session-row > button:first-child small { color: inherit; line-height: 1.3; opacity: .72; font-size: 12px; } +.session-menu-trigger { width: 26px; height: 26px; display: grid; place-items: center; border: 0; border-radius: 999px; background: transparent; color: var(--color-ink-tertiary); cursor: pointer; opacity: 0; transition: opacity 120ms ease-out, background-color 120ms ease-out; } +.session-row:hover .session-menu-trigger, .session-row.is-active .session-menu-trigger, .session-menu-trigger[aria-expanded="true"] { opacity: 1; } +.session-menu-trigger:hover, .session-menu-trigger[aria-expanded="true"] { background: var(--color-canvas); color: var(--color-ink); } +.session-actions { position: absolute; z-index: 4; top: calc(100% - 4px); right: var(--space-1); min-width: 112px; display: grid; gap: 2px; padding: 6px; border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas); box-shadow: var(--shadow-elevated); } +.session-actions button { min-height: 30px; width: 100%; padding: 0 8px; border: 0; border-radius: 8px; background: transparent; color: var(--color-ink-secondary); text-align: left; font-size: 12px; } +.session-actions button:hover { background: var(--color-canvas-muted); color: var(--color-ink); } .sidebar-footer { margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--color-border); } .profile-trigger { width: 100%; display: grid; grid-template-columns: 34px minmax(0, 1fr) 18px; align-items: center; gap: 9px; padding: 5px 7px; border: 0; background: transparent; color: var(--color-ink); cursor: pointer; text-align: left; transition: background-color 120ms ease-out, transform 120ms ease-out; min-height: 56px; border-radius: var(--radius-md); } .profile-initial { width: 32px; height: 32px; display: grid; place-items: center; border-radius: 50%; font-size: 12px; text-transform: uppercase; border: 0; background: var(--color-action); color: var(--color-on-dark); font-weight: 500; } diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 74ba4e51..65a85d49 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -669,6 +669,7 @@ export default function Home() { const [sessions, setSessions] = useState([]); const [pinnedSessionIds, setPinnedSessionIds] = useState([]); const [archivedSessionIds, setArchivedSessionIds] = useState([]); + const [sessionMenuId, setSessionMenuId] = useState(null); const [modelCatalog, setModelCatalog] = useState(null); const [activeSessionId, setActiveSessionId] = useState(""); const [draft, setDraft] = useState(""); @@ -1178,6 +1179,20 @@ export default function Home() { if (activeSessionId === sessionId) setActiveSessionId(visibleSessions.find((session) => session.id !== sessionId)?.id ?? ""); } + async function shareSession(session: ChatSession) { + const transcript = [ + `Jyotisha 对话:${session.title}`, + "", + ...session.messages.map((message) => `${message.role === "user" ? "我" : "Jyotisha"}:${message.text}`), + ].join("\n"); + try { + await navigator.clipboard.writeText(transcript); + setComposerNotice("已复制当前聊天,可粘贴转发。"); + } catch { + setComposerNotice("无法访问剪贴板,请手动复制聊天内容。"); + } + } + async function startNewChat() { if (!account || !modelCatalog || creatingSession) return; setMobileSidebarOpen(false); @@ -1992,13 +2007,21 @@ export default function Home() { 聊天记录
{visibleSessions.map((session) => ( -
+
{ + event.preventDefault(); + setSessionMenuId(sessionMenuId === session.id ? null : session.id); + }} + > -
- - - - -
+ + {sessionMenuId === session.id && ( +
+ + + + + +
+ )}
))}
diff --git a/tests/test_session_management_entrypoints.py b/tests/test_session_management_entrypoints.py index 580aa8ca..04f1460e 100644 --- a/tests/test_session_management_entrypoints.py +++ b/tests/test_session_management_entrypoints.py @@ -11,9 +11,14 @@ def test_chat_history_management_actions_are_exposed() -> None: "deleteSession", "togglePinnedSession", "toggleArchivedSession", + "shareSession", + "onContextMenu", + "session-menu-trigger", + 'role="menu"', "置顶", "重命名", "归档", "删除", + "转发", ): assert expected in source From bbcec4d86dec34d32ec388002a0d5a5a868ecf03 Mon Sep 17 00:00:00 2001 From: 732642856 <732642856@qq.com> Date: Sat, 18 Jul 2026 06:07:45 +0800 Subject: [PATCH 7/8] feat: add archived chat recovery menu --- frontend/src/app/globals.css | 4 +++- frontend/src/app/page.tsx | 12 +++++++++--- tests/test_session_management_entrypoints.py | 2 ++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 768e4632..76114aa8 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -262,7 +262,9 @@ button:disabled { cursor: default; opacity: .45; } .brand-mark, .auth-brand span { width: 32px; height: 32px; border-radius: 50%; background: var(--color-canvas) url("/jyotish-logo.png") center / contain no-repeat; box-shadow: 0 0 0 1px var(--color-border); } .auth-story-brand img { width: 32px; height: 32px; border-radius: 50%; object-fit: contain; box-shadow: 0 0 0 1px var(--color-border); } .new-chat { width: 100%; min-height: 44px; display: flex; align-items: center; justify-content: center; gap: 8px; padding: 0 14px; border: 0; background: var(--color-surface-dark); color: var(--color-on-dark); cursor: pointer; font-size: var(--type-body-sm); transition: background-color 140ms ease-out, transform 120ms ease-out; margin: var(--space-4) 0 var(--space-6); border-radius: var(--radius-md); font-weight: 500; } -.sidebar-label { display: block; padding: 0 var(--space-3) var(--space-2); color: var(--color-ink-tertiary); font-size: var(--type-overline); font-weight: 500; letter-spacing: 1.5px; } +.session-nav-header { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); padding: 0 var(--space-3) var(--space-2); } +.sidebar-label { display: block; color: var(--color-ink-tertiary); font-size: var(--type-overline); font-weight: 500; letter-spacing: 1.5px; } +.session-nav-header button { min-height: 26px; padding: 0 8px; border: 1px solid var(--color-border); border-radius: 999px; background: var(--color-canvas); color: var(--color-ink-secondary); cursor: pointer; font-size: 11px; } .session-list { min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: var(--space-1); } .session-row { position: relative; display: grid; grid-template-columns: minmax(0, 1fr) 30px; align-items: center; border-radius: var(--radius-md); color: var(--color-ink-secondary); } .session-row > button:first-child { position: relative; width: 100%; display: grid; gap: 2px; border: 0; background: transparent; cursor: pointer; text-align: left; transition: background-color 120ms ease-out, color 120ms ease-out, transform 120ms ease-out; min-height: 52px; padding: var(--space-2) var(--space-3) var(--space-2) var(--space-4); border-radius: var(--radius-md); color: inherit; } diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 65a85d49..8424b00b 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -669,6 +669,7 @@ export default function Home() { const [sessions, setSessions] = useState([]); const [pinnedSessionIds, setPinnedSessionIds] = useState([]); const [archivedSessionIds, setArchivedSessionIds] = useState([]); + const [showArchivedSessions, setShowArchivedSessions] = useState(false); const [sessionMenuId, setSessionMenuId] = useState(null); const [modelCatalog, setModelCatalog] = useState(null); const [activeSessionId, setActiveSessionId] = useState(""); @@ -714,7 +715,7 @@ export default function Home() { const activeSession = sessions.find((session) => session.id === activeSessionId) ?? sessions[0]; const visibleSessions = sessions - .filter((session) => !archivedSessionIds.includes(session.id)) + .filter((session) => showArchivedSessions ? archivedSessionIds.includes(session.id) : !archivedSessionIds.includes(session.id)) .sort((left, right) => Number(pinnedSessionIds.includes(right.id)) - Number(pinnedSessionIds.includes(left.id))); const activeError = requestError && requestError.sessionId === activeSession?.id ? requestError.message : ""; const isLoading = pendingSessionId === activeSession?.id; @@ -2004,7 +2005,12 @@ export default function Home() {