mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-28 05:46:58 +02:00
A complete UX upgrade for chat sending → receiving → recovering.
* StatusPill replaces the orphan spinner — stage-aware copy
("Reading files · 12s", "Searching the web · 14s", "Typing · 24s"),
shimmer text, monotonic timer, derived effective status, > 60s
warning tone, > 5min cancel button.
* WS writethrough on task:queued / task:dispatch / task:cancelled so
pendingTask cache stays in sync with the daemon state machine without
invalidate-refetch latency. broadcastTaskDispatch now includes
chat_session_id when the task is for a chat session — the existing
payload only carried it on the generic task: events, leaving the pill
stuck at "Queued" until completion.
* Failure fallback — FailTask writes a chat_message tagged with
failure_reason (mirrors the issue path's system comment, gated on
retried==nil). Front-end renders an inline note ("Connection failed",
with a Show details collapsible) instead of the previous black hole.
* Elapsed timing — chat_message.elapsed_ms persists task.completed_at -
task.created_at on success/failure rows. UI shows "Replied in 38s" /
"Failed after 12s" beneath assistant bubbles. Format helper shared
between StatusPill and the persisted caption so the live timer and
final reading never disagree.
* Optimistic burst rebalanced — pendingTask seed + created_at moved
before the HTTP roundtrip so the pill appears the instant the user
hits send; handleStop is fire-and-forget so cancel feels immediate
(server confirmation arrives via task:cancelled WS).
* Presence integration — chat avatars use ActorAvatar (status dot +
hover card); OfflineBanner above the input on offline/unstable;
SessionDropdown shows per-row in-flight/unread pip plus a
cross-session aggregate pip on the closed trigger.
* Editor blur on send so the caret stops competing with the StatusPill
/ streaming reply for the user's attention.
* Chat panel isOpen now persists globally; defaults to OPEN for new
users (storage key absence) so the feature is discoverable. Existing
users' prior choice is respected.
* DB: migrations 062 (failure_reason) + 063 (elapsed_ms), both
ADD COLUMN NULL — fast, non-blocking, backwards compatible.
* WS: task:failed chat path now invalidates chatKeys.messages — fixes
a pre-existing bug where the failure bubble required a page refresh
to appear.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
153 lines
5.0 KiB
CSS
153 lines
5.0 KiB
CSS
/* =============================================================================
|
|
* Multica shared base styles — imported by all apps
|
|
* ============================================================================= */
|
|
|
|
/* Shiki dual themes: CSS-only light/dark switching via CSS variables */
|
|
/* @see https://shiki.style/guide/dual-themes */
|
|
.shiki,
|
|
.shiki span {
|
|
color: var(--shiki-light);
|
|
}
|
|
|
|
.dark .shiki,
|
|
.dark .shiki span {
|
|
color: var(--shiki-dark) !important;
|
|
}
|
|
|
|
/* Multica icon: entrance spin animation */
|
|
@keyframes entrance-spin {
|
|
0% { transform: rotate(0deg); opacity: 0; }
|
|
50% { opacity: 1; }
|
|
100% { transform: rotate(360deg); opacity: 1; }
|
|
}
|
|
|
|
.animate-entrance-spin {
|
|
animation: entrance-spin 0.6s ease-out forwards;
|
|
}
|
|
|
|
/* Onboarding: step / phase entry — 400ms fade + 4px rise.
|
|
* Matches the design prototype's .fade-in. Applied on mount so every new
|
|
* step (and intra-step phase switch, via key=phase remount) plays once.
|
|
* `both` fill-mode commits the `from` styles pre-animation to avoid a
|
|
* single-frame flash at natural state before the animation grabs. */
|
|
@keyframes onboarding-enter {
|
|
from { opacity: 0; transform: translateY(4px); }
|
|
}
|
|
|
|
.animate-onboarding-enter {
|
|
animation: onboarding-enter 0.4s ease both;
|
|
}
|
|
|
|
/* Onboarding completion: success badge spring-pop.
|
|
* Lands with a subtle overshoot (scale 1.12 → 1) so the circle feels
|
|
* physical rather than linearly interpolated. Paired with the drawn
|
|
* checkmark below which kicks in after the badge has settled. */
|
|
@keyframes completion-badge {
|
|
0% { transform: scale(0); opacity: 0; }
|
|
60% { transform: scale(1.12); opacity: 1; }
|
|
100% { transform: scale(1); opacity: 1; }
|
|
}
|
|
|
|
.animate-completion-badge {
|
|
animation: completion-badge 500ms cubic-bezier(0.5, 1.5, 0.4, 1) both;
|
|
}
|
|
|
|
/* Onboarding completion: SVG checkmark drawn by animating
|
|
* stroke-dashoffset from 1 → 0. Requires the target <path> to declare
|
|
* `pathLength={1}` and `strokeDasharray={1}` so the stroke length is
|
|
* normalized and the animation is geometry-agnostic. */
|
|
@keyframes completion-check {
|
|
from { stroke-dashoffset: 1; }
|
|
to { stroke-dashoffset: 0; }
|
|
}
|
|
|
|
.animate-completion-check {
|
|
animation: completion-check 400ms ease-out 350ms both;
|
|
}
|
|
|
|
/* Chat FAB: gentle color + border tint while a chat task is running.
|
|
* Keeps the ring at the same thickness — only hue shifts towards brand
|
|
* at half-cycle, no outer glow. */
|
|
@keyframes chat-impulse {
|
|
0%, 100% {
|
|
color: var(--muted-foreground);
|
|
box-shadow: 0 0 0 1px color-mix(in oklab, var(--foreground) 10%, transparent);
|
|
}
|
|
50% {
|
|
color: var(--brand);
|
|
box-shadow: 0 0 0 1px color-mix(in oklab, var(--brand) 40%, transparent);
|
|
}
|
|
}
|
|
|
|
.animate-chat-impulse {
|
|
animation: chat-impulse 1.6s ease-in-out infinite;
|
|
}
|
|
|
|
/* ChatGPT-style "thinking" shimmer for inline text — a soft light sweep
|
|
* runs across the glyphs, signalling "the agent is doing something" without
|
|
* a separate spinner. Pure CSS: linear-gradient clipped to the text shape,
|
|
* the gradient slid across via background-position. Uses the same muted →
|
|
* foreground tokens chat copy normally uses, so the effect adapts to light
|
|
* and dark mode without per-mode overrides.
|
|
*
|
|
* Apply to a <span> wrapping the label only — not the whole pill, since
|
|
* the timer counter and Cancel button shouldn't shimmer. */
|
|
@keyframes chat-text-shimmer {
|
|
0% { background-position: 200% 0; }
|
|
100% { background-position: -200% 0; }
|
|
}
|
|
|
|
.animate-chat-text-shimmer {
|
|
background-image: linear-gradient(
|
|
90deg,
|
|
var(--muted-foreground) 0%,
|
|
var(--muted-foreground) 35%,
|
|
var(--foreground) 50%,
|
|
var(--muted-foreground) 65%,
|
|
var(--muted-foreground) 100%
|
|
);
|
|
background-size: 200% 100%;
|
|
background-clip: text;
|
|
-webkit-background-clip: text;
|
|
color: transparent;
|
|
-webkit-text-fill-color: transparent;
|
|
animation: chat-text-shimmer 2.5s linear infinite;
|
|
}
|
|
|
|
/* Sidebar: open triggers (dropdown/popover) get active background */
|
|
[data-sidebar="menu-button"][data-popup-open] {
|
|
background-color: var(--sidebar-accent);
|
|
color: var(--sidebar-accent-foreground);
|
|
}
|
|
|
|
/* Sonner toast: align icon to first line of text, not vertically centered */
|
|
[data-sonner-toast] {
|
|
align-items: flex-start !important;
|
|
}
|
|
|
|
[data-sonner-toast] [data-icon] {
|
|
margin-top: 2.5px;
|
|
}
|
|
|
|
@layer base {
|
|
* {
|
|
@apply border-border outline-ring/50;
|
|
scrollbar-width: thin;
|
|
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
|
|
}
|
|
*::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
*::-webkit-scrollbar-track { background: var(--scrollbar-track); }
|
|
*::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: 3px; }
|
|
*::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover); }
|
|
body {
|
|
@apply bg-background text-foreground;
|
|
}
|
|
html {
|
|
@apply font-sans;
|
|
/* Auto-insert 1/4em space between CJK ideographs and Latin letters/numerals.
|
|
* Native CSS text-autospace (Chrome 119+, Electron recent versions).
|
|
* Progressive enhancement: browsers that don't support it simply ignore the rule. */
|
|
text-autospace: ideograph-alpha ideograph-numeric;
|
|
}
|
|
}
|