feat: refactor coach chat to ChatGPT-style Material You design
- Replaced Gemini-style glass/blue aesthetic with clean Material You pink theme - ChatGPT-style message layout: user right-aligned, coach left-aligned with avatars - User avatar shows initials, coach avatar shows brain icon - Chat bubble design: rounded cards matching app theme (no glass/blur effects) - Clean textarea input with Enter to send, Shift+Enter for newlines - ChatGPT-style empty state with suggestion chips - Typing indicator with bouncing dots animation - Sidebar uses Material You surface/container colors - Status bar shows model name and ready/thinking state - Error messages styled as Material You error containers - Bottom hint text: 'coach can make mistakes' - All colors use CSS custom properties for accent theme consistency
This commit is contained in:
+147
-113
@@ -1785,14 +1785,14 @@ function CoachView({ dashboard, entries, user }: { dashboard: Dashboard; entries
|
|||||||
|
|
||||||
if (!chatKey) {
|
if (!chatKey) {
|
||||||
return (
|
return (
|
||||||
<section className="coach-gemini-shell coach-locked-shell">
|
<section className="coach-shell coach-locked-shell">
|
||||||
<div className="coach-empty-state">
|
<div className="coach-empty-state">
|
||||||
<div className="coach-brand-orb">
|
<div className="coach-empty-icon">
|
||||||
<Lock size={30} aria-hidden="true" />
|
<Lock size={28} aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
<h2>unlock encrypted coach chats</h2>
|
<h2>unlock coach</h2>
|
||||||
<p>
|
<p>
|
||||||
messages encrypt in this browser before appwrite stores them. the passphrase is never saved, so use the same one on every device.
|
messages are encrypted before appwrite stores them. your passphrase is never saved — use the same one on every device.
|
||||||
</p>
|
</p>
|
||||||
<form className="coach-unlock-card" onSubmit={unlockChats}>
|
<form className="coach-unlock-card" onSubmit={unlockChats}>
|
||||||
<input
|
<input
|
||||||
@@ -1808,148 +1808,182 @@ function CoachView({ dashboard, entries, user }: { dashboard: Dashboard; entries
|
|||||||
unlock
|
unlock
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{error && <p className="mt-4 max-w-xl text-sm text-red-100">{error}</p>}
|
{error && <p className="mt-4 max-w-md text-sm" style={{ color: "var(--error)" }}>{error}</p>}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userInitials = user.name
|
||||||
|
? user.name.split(" ").map((n) => n[0]).join("").toUpperCase().slice(0, 2)
|
||||||
|
: (user.email?.[0] ?? "U").toUpperCase();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="coach-gemini-shell">
|
<section className="coach-shell">
|
||||||
<aside className="coach-chat-sidebar">
|
<div className="coach-layout">
|
||||||
<div className="coach-sidebar-brand">
|
<aside className="coach-sidebar">
|
||||||
<div className="coach-brand-orb coach-brand-orb-small">
|
<div className="coach-sidebar-header">
|
||||||
<Brain size={18} aria-hidden="true" />
|
<div className="coach-sidebar-icon">
|
||||||
</div>
|
<Brain size={18} aria-hidden="true" />
|
||||||
<div>
|
|
||||||
<p className="font-semibold text-white">coach</p>
|
|
||||||
<p className="text-xs text-slate-400">{chatStorageStatus}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button className="coach-new-chat" type="button" onClick={startNewChat} disabled={busy}>
|
|
||||||
<MessageSquarePlus size={18} aria-hidden="true" />
|
|
||||||
new chat
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="coach-chat-list">
|
|
||||||
{chats.map((chat) => (
|
|
||||||
<div key={chat.id} className={`coach-chat-row ${chat.id === activeChatId ? "coach-chat-row-active" : ""}`}>
|
|
||||||
<button type="button" onClick={() => setActiveChatId(chat.id)}>
|
|
||||||
<span>{chat.title}</span>
|
|
||||||
<small>{new Intl.DateTimeFormat("en-GB", { day: "2-digit", month: "short" }).format(new Date(chat.updatedAt))}</small>
|
|
||||||
</button>
|
|
||||||
<button type="button" aria-label={`delete ${chat.title}`} onClick={() => void removeChat(chat.id)} disabled={busy}>
|
|
||||||
<Trash2 size={14} aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
<div className="coach-sidebar-label">
|
||||||
</div>
|
<p>coach</p>
|
||||||
|
<p>{chatStorageStatus}</p>
|
||||||
<div className="coach-context-card">
|
|
||||||
<p className="text-xs font-semibold uppercase text-slate-500">today</p>
|
|
||||||
<div className="mt-3 grid gap-2">
|
|
||||||
<WellnessPill label="cans" value={dashboard.todayCans} />
|
|
||||||
<WellnessPill label="caffeine" value={dashboard.todayCaffeine} />
|
|
||||||
<WellnessPill label="favourite" value={dashboard.favouriteFlavour} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<section className="coach-stage">
|
|
||||||
<div className="coach-stage-topbar">
|
|
||||||
<span>{OLLAMA_MODEL}</span>
|
|
||||||
<span>{busy ? "thinking" : "ready"}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="coach-stage-messages" aria-live="polite">
|
|
||||||
{!visibleMessages.length ? (
|
|
||||||
<div className="coach-empty-state">
|
|
||||||
<div className="coach-brand-orb">
|
|
||||||
<Sparkles size={32} aria-hidden="true" />
|
|
||||||
</div>
|
|
||||||
<h2>ready when you are</h2>
|
|
||||||
<p>ask about caffeine pace, sugar, spend, or your flavour pattern. answers stay lower case.</p>
|
|
||||||
<div className="coach-prompt-grid">
|
|
||||||
{quickPrompts.map((prompt) => (
|
|
||||||
<button key={prompt} className="suggestion-chip" type="button" disabled={busy} onClick={() => void sendPrompt(prompt)}>
|
|
||||||
{prompt}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
visibleMessages.map((message) => (
|
|
||||||
<CoachMessageBubble
|
|
||||||
key={message.id}
|
|
||||||
message={message}
|
|
||||||
thinkingOpen={openThinkingIds.includes(message.id)}
|
|
||||||
onToggleThinking={() => toggleThinking(message.id)}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
<div ref={messagesEndRef} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="mx-4 mb-3 rounded-lg border border-red-400/40 bg-red-500/10 px-3 py-2 text-sm text-red-100">
|
|
||||||
{error}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<form className="coach-composer" onSubmit={submit}>
|
<button className="coach-new-chat" type="button" onClick={startNewChat} disabled={busy}>
|
||||||
<button className="composer-icon-button" type="button" onClick={startNewChat} disabled={busy} aria-label="new chat">
|
<Plus size={16} aria-hidden="true" />
|
||||||
<Plus size={22} aria-hidden="true" />
|
new chat
|
||||||
</button>
|
</button>
|
||||||
<input
|
|
||||||
className="coach-input"
|
<div className="coach-chat-list">
|
||||||
value={input}
|
{chats.map((chat) => (
|
||||||
onChange={(event) => setInput(event.target.value)}
|
<div key={chat.id} className={`coach-chat-row ${chat.id === activeChatId ? "coach-chat-row-active" : ""}`}>
|
||||||
placeholder="ask coach"
|
<button type="button" onClick={() => setActiveChatId(chat.id)}>
|
||||||
disabled={busy}
|
<span>{chat.title}</span>
|
||||||
/>
|
<small>{new Intl.DateTimeFormat("en-GB", { day: "2-digit", month: "short" }).format(new Date(chat.updatedAt))}</small>
|
||||||
{busy ? (
|
</button>
|
||||||
<button className="composer-send-button composer-stop-button" type="button" onClick={stopThinking} aria-label="stop thinking">
|
<button type="button" aria-label={`delete ${chat.title}`} onClick={() => void removeChat(chat.id)} disabled={busy}>
|
||||||
<Square size={18} aria-hidden="true" />
|
<Trash2 size={14} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
) : (
|
</div>
|
||||||
<button className="composer-send-button" type="submit" disabled={!input.trim()} aria-label="send coach message">
|
))}
|
||||||
<Send size={20} aria-hidden="true" />
|
</div>
|
||||||
</button>
|
|
||||||
|
<div className="coach-context-card">
|
||||||
|
<p className="text-xs font-semibold uppercase" style={{ color: "var(--muted)" }}>today</p>
|
||||||
|
<div className="mt-2 grid gap-2">
|
||||||
|
<WellnessPill label="cans" value={dashboard.todayCans} />
|
||||||
|
<WellnessPill label="caffeine" value={dashboard.todayCaffeine} />
|
||||||
|
<WellnessPill label="favourite" value={dashboard.favouriteFlavour} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<section className="coach-main">
|
||||||
|
<div className="coach-topbar">
|
||||||
|
<span className="coach-topbar-status">
|
||||||
|
<span className={`coach-topbar-status-dot ${busy ? "coach-topbar-status-dot-busy" : "coach-topbar-status-dot-ready"}`} />
|
||||||
|
{busy ? "thinking" : "ready"}
|
||||||
|
</span>
|
||||||
|
<span className="coach-topbar-status" style={{ color: "var(--muted)" }}>{OLLAMA_MODEL}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="coach-messages" aria-live="polite">
|
||||||
|
<div className="coach-messages-inner">
|
||||||
|
{!visibleMessages.length ? (
|
||||||
|
<div className="coach-empty-state">
|
||||||
|
<div className="coach-empty-icon">
|
||||||
|
<Sparkles size={28} aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<h2>how can I help?</h2>
|
||||||
|
<p>ask about caffeine, sugar, spending, or your flavour patterns.</p>
|
||||||
|
<div className="coach-prompt-grid">
|
||||||
|
{quickPrompts.map((prompt) => (
|
||||||
|
<button key={prompt} className="chat-suggestion-chip" type="button" disabled={busy} onClick={() => void sendPrompt(prompt)}>
|
||||||
|
{prompt}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
visibleMessages.map((message) => (
|
||||||
|
<CoachMessageBubble
|
||||||
|
key={message.id}
|
||||||
|
message={message}
|
||||||
|
userInitials={userInitials}
|
||||||
|
thinkingOpen={openThinkingIds.includes(message.id)}
|
||||||
|
onToggleThinking={() => toggleThinking(message.id)}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="coach-error">
|
||||||
|
<div className="coach-error-inner">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</form>
|
|
||||||
</section>
|
<form className="coach-composer" onSubmit={submit}>
|
||||||
|
<div className="coach-composer-inner">
|
||||||
|
<button className="composer-icon-button" type="button" onClick={startNewChat} disabled={busy} aria-label="new chat">
|
||||||
|
<Plus size={18} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
<textarea
|
||||||
|
className="coach-input"
|
||||||
|
value={input}
|
||||||
|
onChange={(event) => setInput(event.target.value)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === "Enter" && !event.shiftKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
void sendPrompt(input);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder="ask coach"
|
||||||
|
disabled={busy}
|
||||||
|
rows={1}
|
||||||
|
/>
|
||||||
|
{busy ? (
|
||||||
|
<button className="composer-send-button composer-stop-button" type="button" onClick={stopThinking} aria-label="stop thinking">
|
||||||
|
<Square size={16} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button className="composer-send-button" type="submit" disabled={!input.trim()} aria-label="send message">
|
||||||
|
<Send size={16} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="coach-hint">coach can make mistakes. check important info.</p>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CoachMessageBubble({
|
function CoachMessageBubble({
|
||||||
message,
|
message,
|
||||||
|
userInitials,
|
||||||
thinkingOpen,
|
thinkingOpen,
|
||||||
onToggleThinking,
|
onToggleThinking,
|
||||||
}: {
|
}: {
|
||||||
message: CoachMessage;
|
message: CoachMessage;
|
||||||
|
userInitials: string;
|
||||||
thinkingOpen: boolean;
|
thinkingOpen: boolean;
|
||||||
onToggleThinking: () => void;
|
onToggleThinking: () => void;
|
||||||
}) {
|
}) {
|
||||||
const isAssistant = message.role === "assistant";
|
const isAssistant = message.role === "assistant";
|
||||||
const canShowThinking = isAssistant && (message.pending || Boolean(message.thinking));
|
const canShowThinking = isAssistant && (message.pending || Boolean(message.thinking));
|
||||||
const thinkingLabel = message.stopped ? "stopped thinking" : message.pending ? "thinking" : "thinking";
|
const thinkingLabel = message.stopped ? "stopped thinking" : message.pending ? "thinking" : "view reasoning";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article className={`coach-message coach-message-${message.role}`}>
|
<article className={`coach-message ${isAssistant ? "coach-message-assistant" : "coach-message-user"}`}>
|
||||||
|
{isAssistant ? (
|
||||||
|
<div className="coach-message-avatar coach-message-avatar-assistant">
|
||||||
|
<Brain size={16} aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="coach-message-avatar coach-message-avatar-user">
|
||||||
|
{userInitials}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="coach-message-bubble">
|
<div className="coach-message-bubble">
|
||||||
<p className="text-xs font-semibold uppercase text-slate-500">{isAssistant ? "coach" : "you"}</p>
|
<div className="coach-bubble-content">
|
||||||
<div className="mt-2 whitespace-pre-wrap text-sm leading-6 text-white">
|
{message.content || (message.pending ? (
|
||||||
{message.content || (message.pending ? "streaming response..." : "")}
|
<div className="coach-typing-dots"><span /><span /><span /></div>
|
||||||
|
) : "")}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{canShowThinking && (
|
{canShowThinking && (
|
||||||
<div className="mt-3">
|
<div className="mt-2">
|
||||||
<button className={`thinking-slider ${message.pending ? "thinking-slider-active" : ""}`} type="button" onClick={onToggleThinking}>
|
<button className={`thinking-slider ${message.pending ? "thinking-slider-active" : ""}`} type="button" onClick={onToggleThinking}>
|
||||||
<span className="thinking-slider-track">
|
{thinkingLabel}
|
||||||
<span>{thinkingLabel} · click to reveal reasoning</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{thinkingOpen && (
|
{thinkingOpen && (
|
||||||
|
|||||||
+215
-84
@@ -326,62 +326,84 @@ textarea:focus-visible {
|
|||||||
box-shadow: var(--elevation-1);
|
box-shadow: var(--elevation-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.coach-gemini-shell {
|
.coach-shell {
|
||||||
@apply grid min-h-[760px] gap-4;
|
@apply flex flex-col;
|
||||||
|
height: calc(100vh - 200px);
|
||||||
|
min-height: 480px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.coach-shell {
|
||||||
|
height: calc(100vh - 160px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.coach-locked-shell {
|
.coach-locked-shell {
|
||||||
@apply place-items-center;
|
@apply flex items-center justify-center;
|
||||||
grid-template-columns: 1fr !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.coach-chat-sidebar {
|
.coach-layout {
|
||||||
@apply hidden min-h-[760px] flex-col border p-3 xl:flex;
|
@apply relative flex flex-1 gap-4 overflow-hidden;
|
||||||
background: color-mix(in srgb, var(--surface-container-lowest) 80%, transparent);
|
|
||||||
border-color: color-mix(in srgb, var(--outline-variant) 64%, transparent);
|
|
||||||
border-radius: 34px;
|
|
||||||
box-shadow: var(--elevation-1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.coach-sidebar-brand {
|
.coach-sidebar {
|
||||||
@apply flex items-center gap-3 px-2 py-2;
|
@apply hidden w-72 shrink-0 flex-col border p-3 xl:flex;
|
||||||
|
background: var(--surface-container);
|
||||||
|
border-color: var(--outline-variant);
|
||||||
|
border-radius: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.coach-brand-orb {
|
.coach-sidebar-header {
|
||||||
@apply grid h-16 w-16 place-items-center rounded-full shadow-sm;
|
@apply flex items-center gap-3 px-1 py-2;
|
||||||
background: radial-gradient(circle at 30% 26%, #ffffff 0 12%, #d7ecff 13% 48%, #7fb6df 72%, #5d8fb3 100%);
|
|
||||||
color: #163247;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.coach-brand-orb-small {
|
.coach-sidebar-icon {
|
||||||
@apply h-10 w-10;
|
@apply flex h-10 w-10 items-center justify-center rounded-xl;
|
||||||
|
background: var(--primary-container);
|
||||||
|
color: var(--on-primary-container);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coach-sidebar-label {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coach-sidebar-label p:first-child {
|
||||||
|
@apply truncate text-sm font-semibold;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coach-sidebar-label p:last-child {
|
||||||
|
@apply truncate text-xs;
|
||||||
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.coach-new-chat {
|
.coach-new-chat {
|
||||||
@apply mt-4 inline-flex min-h-12 items-center gap-3 rounded-full px-4 text-sm font-semibold transition disabled:cursor-not-allowed;
|
@apply mt-3 inline-flex min-h-11 items-center gap-2 rounded-xl border px-4 text-sm font-semibold transition disabled:cursor-not-allowed;
|
||||||
background: var(--surface-container-high);
|
background: var(--surface-container-high);
|
||||||
|
border-color: var(--outline-variant);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.coach-new-chat:hover:not(:disabled) {
|
.coach-new-chat:hover:not(:disabled) {
|
||||||
background: var(--primary-container);
|
background: var(--primary-container);
|
||||||
|
color: var(--on-primary-container);
|
||||||
}
|
}
|
||||||
|
|
||||||
.coach-chat-list {
|
.coach-chat-list {
|
||||||
@apply mt-4 grid flex-1 content-start gap-1 overflow-y-auto;
|
@apply mt-3 grid flex-1 content-start gap-1 overflow-y-auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.coach-chat-row {
|
.coach-chat-row {
|
||||||
@apply grid grid-cols-[1fr_auto] items-center rounded-3xl transition;
|
@apply grid grid-cols-[1fr_auto] items-center rounded-2xl transition;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.coach-chat-row > button:first-child {
|
.coach-chat-row > button:first-child {
|
||||||
@apply grid min-w-0 gap-1 px-4 py-3 text-left;
|
@apply grid min-w-0 gap-0.5 px-3 py-2.5 text-left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.coach-chat-row > button:last-child {
|
.coach-chat-row > button:last-child {
|
||||||
@apply mr-2 grid h-8 w-8 place-items-center rounded-full opacity-0 transition;
|
@apply mr-1 grid h-7 w-7 place-items-center rounded-lg opacity-0 transition;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,95 +423,156 @@ textarea:focus-visible {
|
|||||||
|
|
||||||
.coach-chat-row-active,
|
.coach-chat-row-active,
|
||||||
.coach-chat-row:hover {
|
.coach-chat-row:hover {
|
||||||
background: var(--surface-container-high);
|
background: var(--primary-container);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coach-chat-row-active {
|
||||||
|
color: var(--on-primary-container);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coach-chat-row-active small {
|
||||||
|
color: var(--on-primary-container);
|
||||||
}
|
}
|
||||||
|
|
||||||
.coach-context-card {
|
.coach-context-card {
|
||||||
@apply mt-4 rounded-[28px] border p-4;
|
@apply mt-auto rounded-2xl border p-3;
|
||||||
background: color-mix(in srgb, var(--surface-container-low) 76%, white);
|
background: var(--surface-container-high);
|
||||||
border-color: var(--outline-variant);
|
border-color: var(--outline-variant);
|
||||||
}
|
}
|
||||||
|
|
||||||
.coach-stage {
|
.coach-main {
|
||||||
@apply relative flex min-h-[760px] flex-col overflow-hidden border;
|
@apply relative flex flex-1 flex-col overflow-hidden border;
|
||||||
background:
|
background: var(--surface-container-lowest);
|
||||||
radial-gradient(circle at 50% 64%, rgba(192, 225, 250, 0.78) 0 18%, transparent 42%),
|
border-color: var(--outline-variant);
|
||||||
linear-gradient(180deg, rgba(255,255,255,0.94), rgba(248,251,255,0.96));
|
border-radius: 28px;
|
||||||
border-color: color-mix(in srgb, var(--outline-variant) 64%, transparent);
|
|
||||||
border-radius: 38px;
|
|
||||||
box-shadow: var(--elevation-1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.coach-stage-topbar {
|
.coach-topbar {
|
||||||
@apply flex items-center justify-between px-5 py-4 text-xs font-semibold;
|
@apply flex items-center justify-between border-b px-4 py-2;
|
||||||
|
border-color: var(--outline-variant);
|
||||||
|
background: var(--surface-container-low);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coach-topbar-status {
|
||||||
|
@apply inline-flex items-center gap-1.5 text-xs font-medium;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.coach-stage-messages {
|
.coach-topbar-status-dot {
|
||||||
@apply flex-1 space-y-5 overflow-y-auto px-4 pb-36 pt-8 sm:px-8 lg:px-16;
|
@apply h-2 w-2 rounded-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coach-topbar-status-dot-ready {
|
||||||
|
background: var(--chart-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coach-topbar-status-dot-busy {
|
||||||
|
background: var(--chart-tertiary);
|
||||||
|
@apply animate-pulse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coach-messages {
|
||||||
|
@apply flex-1 overflow-y-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coach-messages-inner {
|
||||||
|
@apply mx-auto max-w-3xl space-y-4 px-4 pb-32 pt-6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.coach-empty-state {
|
.coach-empty-state {
|
||||||
@apply mx-auto flex min-h-[520px] max-w-3xl flex-col items-center justify-center text-center;
|
@apply flex min-h-full flex-col items-center justify-center px-6 text-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coach-empty-icon {
|
||||||
|
@apply mb-5 flex h-16 w-16 items-center justify-center rounded-2xl;
|
||||||
|
background: var(--primary-container);
|
||||||
|
color: var(--on-primary-container);
|
||||||
}
|
}
|
||||||
|
|
||||||
.coach-empty-state h2 {
|
.coach-empty-state h2 {
|
||||||
@apply mt-6 text-5xl font-normal tracking-tight sm:text-6xl;
|
@apply text-3xl font-semibold tracking-tight;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.coach-empty-state p {
|
.coach-empty-state p {
|
||||||
@apply mt-4 max-w-xl text-base leading-7;
|
@apply mt-2 max-w-md text-sm leading-6;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.coach-prompt-grid {
|
.coach-prompt-grid {
|
||||||
@apply mt-7 grid gap-2 sm:grid-cols-3;
|
@apply mt-6 grid gap-2 sm:grid-cols-1;
|
||||||
|
max-width: 480px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.coach-message {
|
.coach-message {
|
||||||
@apply flex;
|
@apply flex gap-3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.coach-message-user {
|
.coach-message-user {
|
||||||
@apply justify-end;
|
@apply flex-row-reverse;
|
||||||
}
|
}
|
||||||
|
|
||||||
.coach-message-assistant {
|
.coach-message-avatar {
|
||||||
@apply justify-start;
|
@apply flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-xs font-semibold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coach-message-avatar-assistant {
|
||||||
|
background: var(--primary-container);
|
||||||
|
color: var(--on-primary-container);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coach-message-avatar-user {
|
||||||
|
background: var(--tertiary-container);
|
||||||
|
color: var(--on-tertiary-container);
|
||||||
}
|
}
|
||||||
|
|
||||||
.coach-message-bubble {
|
.coach-message-bubble {
|
||||||
@apply max-w-[840px] rounded-[34px] border px-5 py-4 shadow-sm;
|
@apply max-w-[85%] rounded-2xl px-4 py-3;
|
||||||
background: rgba(255, 255, 255, 0.82);
|
}
|
||||||
border-color: color-mix(in srgb, var(--outline-variant) 58%, transparent);
|
|
||||||
backdrop-filter: blur(18px);
|
.coach-message-assistant .coach-message-bubble {
|
||||||
|
background: var(--surface-container-high);
|
||||||
|
color: var(--text);
|
||||||
|
border-bottom-left-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.coach-message-user .coach-message-bubble {
|
.coach-message-user .coach-message-bubble {
|
||||||
background: #ececec;
|
background: var(--primary);
|
||||||
border-color: transparent;
|
color: var(--on-primary);
|
||||||
|
border-bottom-right-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.coach-message-user .coach-message-bubble,
|
.coach-bubble-label {
|
||||||
.coach-message-user .coach-message-bubble * {
|
@apply mb-1 text-xs font-semibold;
|
||||||
color: var(--text) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thinking-slider {
|
|
||||||
@apply w-full overflow-hidden rounded-full border px-3 py-2 text-sm font-medium;
|
|
||||||
background: rgba(255, 255, 255, 0.72);
|
|
||||||
border-color: transparent;
|
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.coach-bubble-content {
|
||||||
|
@apply whitespace-pre-wrap text-sm leading-relaxed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coach-message-user .coach-bubble-content {
|
||||||
|
color: var(--on-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking-slider {
|
||||||
|
@apply mt-2 w-full overflow-hidden rounded-xl border px-3 py-2 text-xs font-medium transition;
|
||||||
|
background: var(--surface-container);
|
||||||
|
border-color: var(--outline-variant);
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking-slider:hover {
|
||||||
|
background: var(--primary-container);
|
||||||
|
color: var(--on-primary-container);
|
||||||
|
}
|
||||||
|
|
||||||
.thinking-slider-active {
|
.thinking-slider-active {
|
||||||
border-color: color-mix(in srgb, var(--primary) 42%, var(--outline-variant));
|
border-color: color-mix(in srgb, var(--primary) 40%, var(--outline-variant));
|
||||||
}
|
}
|
||||||
|
|
||||||
.thinking-slider-track {
|
.thinking-slider-track {
|
||||||
@apply block overflow-hidden whitespace-nowrap;
|
@apply block overflow-hidden whitespace-nowrap;
|
||||||
mask-image: linear-gradient(90deg, transparent, black 18%, black 82%, transparent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.thinking-slider-track span {
|
.thinking-slider-track span {
|
||||||
@@ -499,47 +582,57 @@ textarea:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.thinking-trace {
|
.thinking-trace {
|
||||||
@apply mt-2 max-h-56 overflow-auto rounded-3xl border p-4 text-xs leading-5 whitespace-pre-wrap;
|
@apply mt-2 max-h-56 overflow-auto rounded-xl border p-3 text-xs leading-5 whitespace-pre-wrap;
|
||||||
background: rgba(255, 255, 255, 0.72);
|
background: var(--surface-container);
|
||||||
border-color: color-mix(in srgb, var(--outline-variant) 58%, transparent);
|
border-color: var(--outline-variant);
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.coach-composer {
|
.coach-composer {
|
||||||
@apply absolute inset-x-4 bottom-4 z-10 mx-auto flex max-w-4xl items-center gap-3 rounded-full border p-3 sm:bottom-7;
|
@apply absolute inset-x-0 bottom-0 z-10;
|
||||||
background: rgba(255, 255, 255, 0.94);
|
background: linear-gradient(to top, var(--surface-container-lowest) 60%, transparent);
|
||||||
border-color: color-mix(in srgb, var(--outline-variant) 68%, transparent);
|
padding: 0 1rem 1rem;
|
||||||
box-shadow: 0 18px 50px rgba(74, 102, 122, 0.18);
|
}
|
||||||
backdrop-filter: blur(18px);
|
|
||||||
|
.coach-composer-inner {
|
||||||
|
@apply mx-auto flex max-w-3xl items-end gap-2 rounded-2xl border p-2;
|
||||||
|
background: var(--surface-container-high);
|
||||||
|
border-color: var(--outline-variant);
|
||||||
|
box-shadow: var(--elevation-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.coach-input {
|
.coach-input {
|
||||||
@apply min-h-12 flex-1 rounded-full border-0 px-2 text-lg shadow-none transition;
|
@apply min-h-11 flex-1 resize-none rounded-xl border-0 px-3 py-2 text-sm;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
|
field-sizing: content;
|
||||||
|
max-height: 160px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.coach-input:focus {
|
.coach-input:focus {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.coach-input::placeholder {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
.composer-icon-button,
|
.composer-icon-button,
|
||||||
.composer-send-button {
|
.composer-send-button {
|
||||||
@apply grid h-12 w-12 shrink-0 place-items-center rounded-full transition disabled:cursor-not-allowed disabled:opacity-45;
|
@apply flex h-9 w-9 shrink-0 items-center justify-center rounded-xl transition disabled:cursor-not-allowed disabled:opacity-40;
|
||||||
color: var(--text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.composer-icon-button:hover {
|
.composer-icon-button:hover {
|
||||||
background: var(--surface-container-high);
|
background: var(--surface-container);
|
||||||
}
|
}
|
||||||
|
|
||||||
.composer-send-button {
|
.composer-send-button {
|
||||||
background: #97cbf5;
|
background: var(--primary);
|
||||||
color: #10283a;
|
color: var(--on-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.composer-send-button:hover:not(:disabled) {
|
.composer-send-button:hover:not(:disabled) {
|
||||||
filter: brightness(0.98);
|
filter: brightness(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.composer-stop-button {
|
.composer-stop-button {
|
||||||
@@ -548,15 +641,53 @@ textarea:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.coach-unlock-card {
|
.coach-unlock-card {
|
||||||
@apply mt-8 flex w-full max-w-xl flex-col gap-3 rounded-full border p-3 sm:flex-row;
|
@apply mt-6 flex w-full max-w-md flex-col gap-3;
|
||||||
background: rgba(255, 255, 255, 0.94);
|
}
|
||||||
border-color: color-mix(in srgb, var(--outline-variant) 68%, transparent);
|
|
||||||
box-shadow: var(--elevation-2);
|
.coach-unlock-card .coach-input {
|
||||||
|
@apply rounded-xl border px-4 py-3;
|
||||||
|
background: var(--surface-container-lowest);
|
||||||
|
border-color: var(--outline-variant);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coach-error {
|
||||||
|
@apply mx-auto max-w-3xl px-4 pb-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coach-error-inner {
|
||||||
|
@apply rounded-xl border px-3 py-2 text-sm;
|
||||||
|
border-color: var(--error-container);
|
||||||
|
background: var(--error-container);
|
||||||
|
color: var(--on-error-container);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coach-typing-dots {
|
||||||
|
@apply flex items-center gap-1 py-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coach-typing-dots span {
|
||||||
|
@apply inline-block h-2 w-2 rounded-full;
|
||||||
|
background: var(--muted);
|
||||||
|
animation: coach-bounce 1.4s infinite ease-in-out both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coach-typing-dots span:nth-child(1) { animation-delay: 0s; }
|
||||||
|
.coach-typing-dots span:nth-child(2) { animation-delay: 0.16s; }
|
||||||
|
.coach-typing-dots span:nth-child(3) { animation-delay: 0.32s; }
|
||||||
|
|
||||||
|
@keyframes coach-bounce {
|
||||||
|
0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; }
|
||||||
|
40% { transform: scale(1); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.coach-hint {
|
||||||
|
@apply mt-1.5 text-center text-xs;
|
||||||
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1280px) {
|
@media (min-width: 1280px) {
|
||||||
.coach-gemini-shell {
|
.coach-shell {
|
||||||
grid-template-columns: 340px minmax(0, 1fr);
|
/* sidebar visible */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user