Spaces:
Sleeping
Sleeping
| /** Shared coach source badge, response, debug copy, and server picks. */ | |
| import { animate } from "motion"; | |
| import { useEffect, useRef } from "preact/hooks"; | |
| import { getDebugPaste, type CoachResponse, type ServerPick } from "../api"; | |
| import { prefersReducedMotion, springs } from "../motion/springs"; | |
| import { navigate } from "../router"; | |
| import { Button } from "./Button"; | |
| import { Pressable } from "./Pressable"; | |
| import { useToast } from "./Toast"; | |
| export function sourceLabel(source: CoachResponse["source"]): "Rules" | "Model" { | |
| return source === "model" ? "Model" : "Rules"; | |
| } | |
| export function ServerPicks({ picks }: { picks: ServerPick[] }) { | |
| if (!picks.length) return null; | |
| return ( | |
| <section class="stack compact"> | |
| <h3 class="section-title">From your data</h3> | |
| <div class="pick-list"> | |
| {picks.map((pick) => ( | |
| <Pressable | |
| key={pick.remedy_key} | |
| className="pick-row" | |
| onClick={() => navigate(`/log?remedy=${encodeURIComponent(pick.remedy_key)}`)} | |
| > | |
| <span>{pick.remedy_key}</span> | |
| <span class="muted">n={pick.n} · {Math.round(pick.p_helped * 100)}% helped</span> | |
| </Pressable> | |
| ))} | |
| </div> | |
| </section> | |
| ); | |
| } | |
| export function CoachResultCard({ result }: { result: CoachResponse }) { | |
| const ref = useRef<HTMLElement>(null); | |
| const toast = useToast(); | |
| useEffect(() => { | |
| if (!ref.current) return; | |
| if (prefersReducedMotion()) animate(ref.current, { opacity: [0, 1] }, { duration: 0.18 }); | |
| else animate(ref.current, { opacity: [0, 1], y: [12, 0] }, springs.snap); | |
| }, [result.trace_id]); | |
| const copyDebug = async () => { | |
| try { | |
| await navigator.clipboard.writeText(await getDebugPaste()); | |
| toast.show("Debug copied"); | |
| } catch (err) { | |
| toast.show(err instanceof Error ? err.message : "Copy failed", "error"); | |
| } | |
| }; | |
| const looksLikeTemplate = | |
| /LOOP:\s*</i.test(result.text) || | |
| /<(?:tag|word|one concrete action)/i.test(result.text); | |
| const displayText = looksLikeTemplate | |
| ? "Coach returned an empty template — using Rules backup on retry. Tap Coach again or force Rules." | |
| : result.text; | |
| return ( | |
| <article ref={ref} class="surface-card coach-result stack"> | |
| <div class="result-heading"> | |
| <span class={`source-badge ${sourceLabel(result.source).toLowerCase()}`}> | |
| {looksLikeTemplate ? "Rules" : sourceLabel(result.source)} | |
| </span> | |
| {result.flags.length ? <span class="flags">{result.flags.join(" · ")}</span> : null} | |
| </div> | |
| <p class="coach-text">{displayText}</p> | |
| <ServerPicks picks={result.server_picks} /> | |
| <Button variant="secondary" onClick={copyDebug}>Copy debug</Button> | |
| </article> | |
| ); | |
| } | |