File size: 2,763 Bytes
391340f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57ed4c2
 
 
 
 
 
 
 
391340f
 
 
 
57ed4c2
391340f
 
 
57ed4c2
391340f
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
/** 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>
  );
}