| 1 | /** |
| 2 | * Home's way into Ask (components/ask-composer): the box at the top of the |
| 3 | * page, Summarize on what happened in the span, and Explain on each change |
| 4 | * that landed. Each hands its question to the top bar's Ask panel |
| 5 | * (lib/ask.ts askG1t), which asks g1t in the viewer's direct message with |
| 6 | * it, so the answer streams there and stays in Chat. |
| 7 | */ |
| 8 | import { ArrowUp, Sparkles } from "lucide-react"; |
| 9 | import { type FormEvent, type KeyboardEvent, useState } from "react"; |
| 10 | |
| 11 | import { askG1t } from "../lib/ask"; |
| 12 | import { type LandedChange, type Span, explainQuestion, homeStarters, summarizeQuestion } from "../lib/home"; |
| 13 | import { Suggestion, Suggestions } from "./ai-elements/suggestion"; |
| 14 | import { MarkGlyph } from "./logo"; |
| 15 | import { Button } from "./ui/button"; |
| 16 | import { Hint } from "./ui/hint"; |
| 17 | import { Textarea } from "./ui/textarea"; |
| 18 | |
| 19 | type AskSpan = Pick<Span, "key" | "note" | "words" | "from">; |
| 20 | |
| 21 | /** The top of Home: what are we building today, a box that asks g1t, and a few things to ask first. */ |
| 22 | export function HomeAsk({ span, tz }: { span: AskSpan; tz: string | null }) { |
| 23 | const [draft, setDraft] = useState(""); |
| 24 | const send = () => { |
| 25 | if (!draft.trim()) return; |
| 26 | askG1t(draft); |
| 27 | setDraft(""); |
| 28 | }; |
| 29 | const submit = (event: FormEvent) => { |
| 30 | event.preventDefault(); |
| 31 | send(); |
| 32 | }; |
| 33 | const onKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => { |
| 34 | if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) { |
| 35 | event.preventDefault(); |
| 36 | send(); |
| 37 | } |
| 38 | }; |
| 39 | return ( |
| 40 | <section aria-labelledby="home-ask" className="mx-auto flex w-full max-w-3xl flex-col items-center gap-5 pt-4 pb-2 md:pt-8"> |
| 41 | <h1 id="home-ask" className="text-center text-[1.75rem] leading-tight font-semibold tracking-tight md:text-[2rem]"> |
| 42 | What are we building today? |
| 43 | </h1> |
| 44 | <form onSubmit={submit} className="w-full"> |
| 45 | <div className="flex items-end gap-2 rounded-2xl border border-line bg-surface p-2 pl-4 shadow-sm transition-shadow focus-within:border-accent-dim focus-within:ring-2 focus-within:ring-accent/25"> |
| 46 | <MarkGlyph className="mb-2.5 h-4 shrink-0" /> |
| 47 | <label htmlFor="home-ask-box" className="sr-only"> |
| 48 | Ask g1t |
| 49 | </label> |
| 50 | <Textarea |
| 51 | id="home-ask-box" |
| 52 | value={draft} |
| 53 | onChange={(event) => setDraft(event.target.value)} |
| 54 | onKeyDown={onKeyDown} |
| 55 | rows={1} |
| 56 | maxLength={16_000} |
| 57 | placeholder="Ask g1t what changed, what's blocked, or what to do next" |
| 58 | className="field-sizing-content max-h-40 min-h-10 resize-none border-0 bg-transparent px-1 py-2 text-[0.9375rem] shadow-none focus-visible:ring-0" |
| 59 | /> |
| 60 | <Hint label="Ask (Enter)"> |
| 61 | <Button type="submit" variant="accent" size="icon-sm" disabled={!draft.trim()} aria-label="Ask" className="mb-1"> |
| 62 | <ArrowUp size={15} /> |
| 63 | </Button> |
| 64 | </Hint> |
| 65 | </div> |
| 66 | </form> |
| 67 | <Suggestions className="justify-center"> |
| 68 | {homeStarters(span, tz).map((starter) => ( |
| 69 | <Suggestion key={starter.label} suggestion={starter.label} onPick={() => askG1t(starter.question)} /> |
| 70 | ))} |
| 71 | </Suggestions> |
| 72 | </section> |
| 73 | ); |
| 74 | } |
| 75 | |
| 76 | /** Summarize what happened in the span, in plain English. */ |
| 77 | export function SummarizeButton({ span, tz }: { span: AskSpan; tz: string | null }) { |
| 78 | return ( |
| 79 | <Hint label="g1t reads everything that happened in this span and tells you what changed"> |
| 80 | <Button type="button" variant="soft" size="sm" onClick={() => askG1t(summarizeQuestion(span, tz))}> |
| 81 | <Sparkles size={14} /> |
| 82 | Summarize |
| 83 | </Button> |
| 84 | </Hint> |
| 85 | ); |
| 86 | } |
| 87 | |
| 88 | /** Explain one change that landed: what it did and why, in plain English. */ |
| 89 | export function ExplainButton({ change }: { change: Pick<LandedChange, "kind" | "title" | "to"> }) { |
| 90 | return ( |
| 91 | <Hint label="Explain this change"> |
| 92 | <Button |
| 93 | type="button" |
| 94 | variant="ghost" |
| 95 | size="icon-xs" |
| 96 | aria-label={`Explain ${change.title}`} |
| 97 | onClick={() => askG1t(explainQuestion(change))} |
| 98 | className="shrink-0 text-muted hover:text-fg sm:opacity-0 sm:group-hover:opacity-100 sm:focus-visible:opacity-100" |
| 99 | > |
| 100 | <Sparkles size={13} /> |
| 101 | </Button> |
| 102 | </Hint> |
| 103 | ); |
| 104 | } |