From 11631bf044fcb8dc74b7c7dc392d1e67858f0be0 Mon Sep 17 00:00:00 2001 From: Shakker Date: Sun, 31 May 2026 19:30:01 +0100 Subject: [PATCH] feat: animate Skill Workshop chat landing --- ui/src/styles/layout.css | 51 +++++++++++++++++++++++++++++++++++++ ui/src/ui/app-render.ts | 15 +++++++++++ ui/src/ui/app-view-state.ts | 2 ++ ui/src/ui/app.ts | 2 ++ 4 files changed, 70 insertions(+) diff --git a/ui/src/styles/layout.css b/ui/src/styles/layout.css index c6777fbfa48..ccbf6104cf5 100644 --- a/ui/src/styles/layout.css +++ b/ui/src/styles/layout.css @@ -1315,6 +1315,57 @@ margin-top: 0; } +@media (prefers-reduced-motion: no-preference) { + .content--chat-workshop-handoff .content-header { + animation: workshop-chat-header-enter 340ms cubic-bezier(0.16, 1, 0.3, 1) both; + } + + .content--chat-workshop-handoff .chat-thread { + animation: workshop-chat-thread-enter 420ms cubic-bezier(0.16, 1, 0.3, 1) both; + } + + .content--chat-workshop-handoff .agent-chat__input { + animation: workshop-chat-composer-enter 520ms cubic-bezier(0.16, 1, 0.3, 1) 90ms both; + } +} + +@keyframes workshop-chat-header-enter { + from { + opacity: 0; + transform: translateY(-6px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes workshop-chat-thread-enter { + from { + opacity: 0; + transform: translateY(12px) scale(0.992); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes workshop-chat-composer-enter { + 0% { + opacity: 0; + transform: translateY(18px) scale(0.988); + } + 65% { + opacity: 1; + transform: translateY(-2px) scale(1); + } + 100% { + opacity: 1; + transform: translateY(0) scale(1); + } +} + .content--workboard { display: flex; flex-direction: column; diff --git a/ui/src/ui/app-render.ts b/ui/src/ui/app-render.ts index 0654e47d05c..d5d69d59047 100644 --- a/ui/src/ui/app-render.ts +++ b/ui/src/ui/app-render.ts @@ -502,6 +502,7 @@ const SKILL_WORKSHOP_CURRENT_CHAT_REVISIONS_KEY = "openclaw:control-ui:skill-workshop-current-chat-revisions:v1"; const SKILL_WORKSHOP_REVISION_SESSIONS_KEY = "openclaw:control-ui:skill-workshop-revision-sessions:v1"; +const SKILL_WORKSHOP_CHAT_HANDOFF_MS = 900; const MAX_SKILL_WORKSHOP_REVIEWED_KEYS = 500; const MAX_SKILL_WORKSHOP_REVISION_SESSIONS = 200; const DEFAULT_SKILL_WORKSHOP_QUEUE_WIDTH = 360; @@ -917,6 +918,7 @@ async function sendSkillWorkshopRevisionRequest( if (!sessionKey) { throw new Error(state.sessionsError ?? "Could not prepare a Skill Workshop session."); } + startSkillWorkshopChatHandoff(state); if (state.tab !== "chat") { state.setTab("chat" as Tab); } @@ -928,6 +930,17 @@ async function sendSkillWorkshopRevisionRequest( await state.handleSendChat(message); } +function startSkillWorkshopChatHandoff(state: AppViewState): void { + if (state.skillWorkshopChatHandoffTimer) { + globalThis.clearTimeout(state.skillWorkshopChatHandoffTimer); + } + state.skillWorkshopChatHandoffActive = true; + state.skillWorkshopChatHandoffTimer = globalThis.setTimeout(() => { + state.skillWorkshopChatHandoffActive = false; + state.skillWorkshopChatHandoffTimer = null; + }, SKILL_WORKSHOP_CHAT_HANDOFF_MS); +} + function loadDismissedUpdateBanner(): DismissedUpdateBanner | null { try { const raw = getSafeLocalStorage()?.getItem(UPDATE_BANNER_DISMISS_KEY); @@ -2371,6 +2384,8 @@ export function renderApp(state: AppViewState) {
| number | null; + skillWorkshopChatHandoffActive?: boolean; + skillWorkshopChatHandoffTimer?: ReturnType | number | null; healthLoading: boolean; healthResult: HealthSummary | null; healthError: string | null; diff --git a/ui/src/ui/app.ts b/ui/src/ui/app.ts index 8be08f7d4a0..c53d404d5b0 100644 --- a/ui/src/ui/app.ts +++ b/ui/src/ui/app.ts @@ -657,6 +657,8 @@ export class OpenClawApp extends LitElement { @state() skillWorkshopRevisionKey: string | null = null; @state() skillWorkshopRevisionDraft = ""; skillWorkshopActionNoticeTimer: ReturnType | number | null = null; + @state() skillWorkshopChatHandoffActive = false; + skillWorkshopChatHandoffTimer: ReturnType | number | null = null; @state() healthLoading = false; @state() healthResult: HealthSummary | null = null;