Conversational follow-up on a suggestion, streamed token-by-token. Backend (interface-only; handlers never touch a concrete LLM client): - internal/llm/chat.go: StreamAskPetal with conversational sampling (max_tokens 512, temp 0.7, rep 1.15, top_p 0.92, stop "\n\n\n"), reusing AskPetalSystemPrompt + TrimHistory. - internal/suggestions/chat.go: POST /api/suggestions/:id/chat. One user-scoped join loads the suggestion + parent content_text; surroundingParagraph extracts the \n\n-bounded paragraph at from_pos (whole-doc fallback when unlocated) and injects it server-side. Streams event: token / event: done SSE frames with JSON-encoded data so token newlines can't break framing; real http.Flusher per chunk. LLM-unreachable -> 502 before SSE headers; unknown suggestion -> 404. Frontend: - streamSuggestionChat: fetch + ReadableStream SSE parser (not EventSource, needs POST), abortable. - AskPetal.tsx: whole conversation in component state (no persistence, cleared on close), Petal's first bubble pre-seeded with the explanation, rose/lavender bubbles, CJK font stack on the bubbles only (Note #17), streaming caret. - SuggestionCard "Ask Petal" pill pins the card open while chatting (hover-close suppressed, click-away closes) and widens it to 340px. Tests: chat_test.go covers streamed-text concat + done event, server-side context injection on the system message, sampling params, 404, and surroundingParagraph. go build/vet/test clean, tsc clean, vite build OK. Live SSE smoke-tested against a fake streaming vLLM: tokens flushed individually through the chi middleware stack, done terminator, 502 on LLM-down, 404 on unknown suggestion. Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
153 lines
4.6 KiB
CSS
153 lines
4.6 KiB
CSS
@import "tailwindcss";
|
|
|
|
/* Petal design tokens — soft, warm, bubbly stationery aesthetic.
|
|
Defined in @theme so they generate Tailwind utilities (bg-bg, text-plum,
|
|
rounded-card, font-ui, etc.). See petal-spec.md → Design System. */
|
|
@theme {
|
|
/* Palette */
|
|
--color-bg: #FDF6F0; /* warm cream canvas */
|
|
--color-surface: #FFFFFF; /* card/panel surfaces */
|
|
--color-surface-alt: #FFF0F5; /* rose-tinted alt surface */
|
|
--color-border: #F0E0EB; /* soft pink-grey border */
|
|
--color-plum: #3D2E39; /* warm dark plum text (not harsh black) */
|
|
--color-muted: #9B8CA3; /* muted lavender-grey */
|
|
--color-accent: #E8A0BF; /* soft rose — primary interactive */
|
|
--color-accent-hover: #D98AAF;/* deeper rose on hover */
|
|
|
|
/* Suggestion type colors */
|
|
--color-mint: #A8D8C8; /* grammar */
|
|
--color-peach: #F4B8A0; /* phrasing */
|
|
--color-lavender: #C5B4E8; /* idiom */
|
|
--color-sky: #A8CCE8; /* clarity */
|
|
--color-honey: #CE9B4F; /* voice */
|
|
--color-success: #8FCFA8; /* saved / accepted */
|
|
|
|
/* Typography */
|
|
--font-ui: "Nunito", "PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;
|
|
--font-body: "Lora", Georgia, serif;
|
|
--font-mono: "JetBrains Mono", ui-monospace, monospace;
|
|
|
|
/* Shape language */
|
|
--radius-card: 16px;
|
|
--radius-modal: 24px;
|
|
--radius-pill: 999px;
|
|
--radius-input: 12px;
|
|
|
|
/* Soft, warm, lifted shadow */
|
|
--shadow-soft: 0 4px 20px rgba(180, 130, 160, 0.12);
|
|
}
|
|
|
|
html, body, #root {
|
|
height: 100%;
|
|
}
|
|
|
|
body {
|
|
margin: 0;
|
|
background: var(--color-bg);
|
|
color: var(--color-plum);
|
|
font-family: var(--font-ui);
|
|
-webkit-font-smoothing: antialiased;
|
|
}
|
|
|
|
/* Gentle, consistent motion across interactive elements */
|
|
button, a, input {
|
|
transition: all 200ms ease;
|
|
}
|
|
|
|
/* Editor body — serif Lora for prose, warm plum ink, roomy line height. The
|
|
title and headings use the Nunito UI face for contrast. */
|
|
.petal-prose {
|
|
font-family: var(--font-body);
|
|
font-size: 1.125rem;
|
|
line-height: 1.75;
|
|
color: var(--color-plum);
|
|
}
|
|
.petal-prose > * + * {
|
|
margin-top: 0.9em;
|
|
}
|
|
.petal-prose h1,
|
|
.petal-prose h2,
|
|
.petal-prose h3 {
|
|
font-family: var(--font-ui);
|
|
font-weight: 800;
|
|
line-height: 1.3;
|
|
}
|
|
.petal-prose h1 { font-size: 1.6em; }
|
|
.petal-prose h2 { font-size: 1.3em; }
|
|
.petal-prose h3 { font-size: 1.1em; }
|
|
.petal-prose ul,
|
|
.petal-prose ol {
|
|
padding-left: 1.4em;
|
|
}
|
|
.petal-prose ul { list-style: disc; }
|
|
.petal-prose ol { list-style: decimal; }
|
|
.petal-prose a {
|
|
color: var(--color-accent-hover);
|
|
text-decoration: underline;
|
|
}
|
|
.petal-prose blockquote {
|
|
border-left: 3px solid var(--color-border);
|
|
padding-left: 1em;
|
|
color: var(--color-muted);
|
|
font-style: italic;
|
|
}
|
|
.petal-prose code {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.9em;
|
|
background: var(--color-surface-alt);
|
|
padding: 0.1em 0.35em;
|
|
border-radius: 6px;
|
|
}
|
|
/* Placeholder shown on the empty first paragraph (Tiptap Placeholder ext). */
|
|
.petal-prose p.is-editor-empty:first-child::before {
|
|
content: attr(data-placeholder);
|
|
color: var(--color-muted);
|
|
float: left;
|
|
height: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* --- Suggestion decorations -------------------------------------------------
|
|
Inline highlights anchored by string at render time (not stored marks). Each
|
|
type gets a soft underline in its palette color; hovering opens its card. */
|
|
.petal-suggestion {
|
|
border-bottom: 2px solid transparent;
|
|
border-radius: 2px;
|
|
cursor: pointer;
|
|
transition: background 200ms ease;
|
|
/* gentle fade + slight upward float as decorations appear */
|
|
animation: petal-suggestion-in 260ms ease both;
|
|
}
|
|
.petal-suggestion:hover {
|
|
background: var(--color-surface-alt);
|
|
}
|
|
.petal-suggestion-grammar { border-bottom-color: var(--color-mint); }
|
|
.petal-suggestion-phrasing { border-bottom-color: var(--color-peach); }
|
|
.petal-suggestion-idiom { border-bottom-color: var(--color-lavender); }
|
|
.petal-suggestion-clarity { border-bottom-color: var(--color-sky); }
|
|
.petal-suggestion-voice { border-bottom-color: var(--color-honey); }
|
|
|
|
@keyframes petal-suggestion-in {
|
|
from { opacity: 0; transform: translateY(4px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
.petal-suggestion-card {
|
|
animation: petal-suggestion-in 200ms ease both;
|
|
}
|
|
|
|
/* Blinking caret in the Ask Petal bubble while awaiting the first token. */
|
|
.petal-chat-caret {
|
|
animation: petal-breathe 1s ease-in-out infinite;
|
|
color: var(--color-accent);
|
|
}
|
|
|
|
/* Breathing rose dot shown in the StatusBar while a checkpoint runs. */
|
|
.petal-checkpoint-dot {
|
|
animation: petal-breathe 2s ease-in-out infinite;
|
|
}
|
|
@keyframes petal-breathe {
|
|
0%, 100% { opacity: 0.4; }
|
|
50% { opacity: 1; }
|
|
}
|