Files
petal/web/src/index.css
prosolis 3c5f3ecb96 Phase 4: Ask Petal SSE chat
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
2026-06-25 21:05:39 -07:00

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; }
}