Home opens with an Ask box (#51): "What are we building today?" over a box that asks g1t, with starters (what changed since I was last here, what needs me first, what shipped today, who's working on what), in place of "Welcome back". Summarize, beside the span switch on "Since you were last here", asks g1t what changed in that span in plain English, from its start in the viewer's zone; Explain on each Landed row asks what that one change did, why, and whether it is live. Each hands its question to the top bar's Ask (askG1t, a g1t:ask window event), which opens and sends it, or leaves it in the box when Ask is not set up or an answer is still coming. Checked: the site's typecheck and tests (956), and the real Home route and Ask panel rendered with mocked data at desktop and phone width, dark and light, clicking the box, a starter, Summarize and Explain.
- G1t-Session:
- ses_01m4ygex20ezftekecszgw2gv5
8 files+266−370/8 viewed
| 9 | 9 | import { Suspense, lazy, useEffect, useState } from "react"; | |
| 10 | 10 | import { useLocation } from "react-router"; | |
| 11 | 11 | ||
| 12 | + | import { ASK_EVENT } from "../lib/ask"; | |
| 12 | 13 | import { MarkGlyph } from "./logo"; | |
| 13 | 14 | import { Button } from "./ui/button"; | |
| 14 | 15 | import { Hint } from "./ui/hint"; | |
| ⋯ | |||
| 21 | 22 | export function AskButton({ slug }: { slug: string }) { | |
| 22 | 23 | const [open, setOpen] = useState(false); | |
| 23 | 24 | const [wanted, setWanted] = useState(false); | |
| 25 | + | // A question a page asked (lib/ask.ts askG1t), until the panel sends it. | |
| 26 | + | const [queued, setQueued] = useState<{ text: string; at: number } | null>(null); | |
| 24 | 27 | const location = useLocation(); | |
| 25 | 28 | // Any completed navigation, including answer citations and the current DM, | |
| 26 | 29 | // reveals its destination. Revalidation and links opened in another tab do not. | |
| 27 | 30 | useEffect(() => setOpen(false), [location.key]); | |
| 31 | + | useEffect(() => { | |
| 32 | + | const onAsk = (event: Event) => { | |
| 33 | + | const text = (event as CustomEvent<{ text?: string }>).detail?.text?.trim(); | |
| 34 | + | if (!text) return; | |
| 35 | + | setQueued({ text, at: Date.now() }); | |
| 36 | + | setWanted(true); | |
| 37 | + | setOpen(true); | |
| 38 | + | }; | |
| 39 | + | window.addEventListener(ASK_EVENT, onAsk); | |
| 40 | + | return () => window.removeEventListener(ASK_EVENT, onAsk); | |
| 41 | + | }, []); | |
| 28 | 42 | const change = (next: boolean) => { | |
| 29 | 43 | if (next) setWanted(true); | |
| 30 | 44 | setOpen(next); | |
| ⋯ | |||
| 41 | 55 | </Hint> | |
| 42 | 56 | {wanted && ( | |
| 43 | 57 | <Suspense fallback={<Loading />}> | |
| 44 | − | <AskSheet slug={slug} open={open} /> | |
| 58 | + | <AskSheet slug={slug} open={open} queued={queued} onQueuedTaken={() => setQueued(null)} /> | |
| 45 | 59 | </Suspense> | |
| 46 | 60 | )} | |
| 47 | 61 | </Sheet> | |
| 223 | 223 | ||
| 224 | 224 | type G1tConversation = ReturnType<typeof useG1tConversation>; | |
| 225 | 225 | ||
| 226 | + | /** A question another page asked (lib/ask.ts askG1t), waiting for the panel to send it. */ | |
| 227 | + | type Queued = { text: string; at: number }; | |
| 228 | + | ||
| 226 | 229 | /** | |
| 227 | 230 | * The Ask panel's sheet (components/ask-button.tsx loads it the first time | |
| 228 | 231 | * the top bar's Ask opens, so pages don't carry it until then): the | |
| 229 | 232 | * conversation, kept while the panel is closed. | |
| 230 | 233 | */ | |
| 231 | − | export function AskSheet({ slug, open }: { slug: string; open: boolean }) { | |
| 234 | + | export function AskSheet({ slug, open, queued = null, onQueuedTaken }: { slug: string; open: boolean; queued?: Queued | null; onQueuedTaken?: () => void }) { | |
| 232 | 235 | const conversation = useG1tConversation(slug, open); | |
| 233 | 236 | const busy = anyAnswering(conversation.messages); | |
| 234 | 237 | return ( | |
| ⋯ | |||
| 242 | 245 | void conversation.act("g1t_stop"); | |
| 243 | 246 | }} | |
| 244 | 247 | > | |
| 245 | − | <AskPanel slug={slug} open={open} conversation={conversation} /> | |
| 248 | + | <AskPanel slug={slug} open={open} conversation={conversation} queued={queued} onQueuedTaken={onQueuedTaken} /> | |
| 246 | 249 | </SheetContent> | |
| 247 | 250 | ); | |
| 248 | 251 | } | |
| ⋯ | |||
| 253 | 256 | return null; | |
| 254 | 257 | } | |
| 255 | 258 | ||
| 256 | − | function AskPanel({ slug, open, conversation }: { slug: string; open: boolean; conversation: G1tConversation }) { | |
| 259 | + | function AskPanel({ | |
| 260 | + | slug, | |
| 261 | + | open, | |
| 262 | + | conversation, | |
| 263 | + | queued, | |
| 264 | + | onQueuedTaken, | |
| 265 | + | }: { | |
| 266 | + | slug: string; | |
| 267 | + | open: boolean; | |
| 268 | + | conversation: G1tConversation; | |
| 269 | + | queued: Queued | null; | |
| 270 | + | onQueuedTaken?: () => void; | |
| 271 | + | }) { | |
| 257 | 272 | const { pathname } = useLocation(); | |
| 258 | 273 | const { channelId, messages, loaded, problem, ask, act, clear, load, onEvent } = conversation; | |
| 259 | 274 | const [clearing, setClearing] = useState(false); | |
| ⋯ | |||
| 335 | 350 | send(draft); | |
| 336 | 351 | } | |
| 337 | 352 | }; | |
| 353 | + | // A question asked from a page (Home's box, Summarize, Explain): sent once | |
| 354 | + | // Ask is ready and the conversation has loaded; left in the box when Ask is | |
| 355 | + | // not set up or an answer is still coming, so nothing is lost. | |
| 356 | + | useEffect(() => { | |
| 357 | + | if (!queued || !open || !loaded || (setup == null && !setupError)) return; | |
| 358 | + | if (ready && !busy) void ask(queued.text, pathname); | |
| 359 | + | else setDraft(queued.text); | |
| 360 | + | onQueuedTaken?.(); | |
| 361 | + | // eslint-disable-next-line react-hooks/exhaustive-deps -- once per queued question | |
| 362 | + | }, [queued?.at, open, loaded, setup, setupError]); | |
| 363 | + | ||
| 338 | 364 | const fresh = () => { | |
| 339 | 365 | void act("g1t_reset"); | |
| 340 | 366 | input.current?.focus(); | |
| 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 | + | } |
| 53 | 53 | import { cn } from "../lib/cn"; | |
| 54 | 54 | import { money } from "../lib/money"; | |
| 55 | 55 | import { CommitText } from "./commit-message"; | |
| 56 | + | import { ExplainButton } from "./home-ask"; | |
| 56 | 57 | import { ButtonLink } from "./ui"; | |
| 57 | 58 | import { AgentMarker, Avatar } from "./ui/avatar"; | |
| 58 | 59 | import { Badge, type BadgeTone } from "./ui/badge"; | |
| ⋯ | |||
| 545 | 546 | ||
| 546 | 547 | function LandedItem({ change, now, tz, timeOnly }: { change: LandedChange; now: number; tz: string | null; timeOnly?: boolean }) { | |
| 547 | 548 | return ( | |
| 548 | − | <li className={cn(rowClass, "items-start")}> | |
| 549 | + | <li className={cn(rowClass, "group items-start")}> | |
| 549 | 550 | <span className="mt-0.5 shrink-0">{LANDED_ICON[change.kind]}</span> | |
| 550 | 551 | <div className="min-w-0 grow"> | |
| 551 | 552 | <div className="flex min-w-0 items-center gap-1.5"> | |
| ⋯ | |||
| 566 | 567 | <Shipped change={change} /> | |
| 567 | 568 | </div> | |
| 568 | 569 | </div> | |
| 570 | + | <ExplainButton change={change} /> | |
| 569 | 571 | <When at={change.at} now={now} tz={tz} timeOnly={timeOnly} /> | |
| 570 | 572 | </li> | |
| 571 | 573 | ); | |
| 26 | 26 | return page; | |
| 27 | 27 | } | |
| 28 | 28 | ||
| 29 | + | /** The window event a page sends to have the top bar's Ask open and ask a question (components/ask-button.tsx). */ | |
| 30 | + | export const ASK_EVENT = "g1t:ask"; | |
| 31 | + | ||
| 32 | + | /** | |
| 33 | + | * Opens the top bar's Ask panel and asks g1t this, as if it were typed | |
| 34 | + | * there: Home's box, its Summarize and Explain buttons. Where Ask is not | |
| 35 | + | * set up, the panel opens with the question in its box. | |
| 36 | + | */ | |
| 37 | + | export function askG1t(text: string) { | |
| 38 | + | const question = text.trim(); | |
| 39 | + | if (!question) return; | |
| 40 | + | window.dispatchEvent(new CustomEvent<{ text: string }>(ASK_EVENT, { detail: { text: question } })); | |
| 41 | + | } | |
| 42 | + | ||
| 29 | 43 | /** What the composer says about where the question is asked from. */ | |
| 30 | 44 | export function askWhere(page: AskPage): string | null { | |
| 31 | 45 | if (page.issue) return `On issue #${page.issue}`; |
| 12 | 12 | agentLabel, | |
| 13 | 13 | attention, | |
| 14 | 14 | awayOf, | |
| 15 | − | landedByDay, | |
| 16 | − | pushRows, | |
| 17 | − | sessionTrailers, | |
| 18 | − | settledIn, | |
| 19 | − | shippedAsks, | |
| 20 | − | shippedLabel, | |
| 21 | − | withSessions, | |
| 22 | − | withShipped, | |
| 23 | 15 | chatRow, | |
| 24 | 16 | codeRow, | |
| 25 | 17 | dayIn, | |
| 26 | 18 | deploysIn, | |
| 27 | 19 | digestOf, | |
| 28 | 20 | duration, | |
| 21 | + | explainQuestion, | |
| 22 | + | homeStarters, | |
| 29 | 23 | landed, | |
| 24 | + | landedByDay, | |
| 30 | 25 | landedDeploys, | |
| 31 | 26 | landedFromActivity, | |
| 32 | 27 | landedIn, | |
| 33 | 28 | notificationRow, | |
| 34 | 29 | parseWindow, | |
| 30 | + | pushRows, | |
| 35 | 31 | rankAttention, | |
| 36 | 32 | running, | |
| 37 | 33 | runningDeploys, | |
| 38 | 34 | runningWorkflows, | |
| 35 | + | sessionTrailers, | |
| 36 | + | settledIn, | |
| 37 | + | shippedAsks, | |
| 38 | + | shippedLabel, | |
| 39 | 39 | sinceWords, | |
| 40 | 40 | spanFor, | |
| 41 | 41 | spanMonths, | |
| 42 | 42 | spanSentence, | |
| 43 | 43 | spendIn, | |
| 44 | 44 | startHere, | |
| 45 | + | summarizeQuestion, | |
| 45 | 46 | waitingSentence, | |
| 47 | + | withSessions, | |
| 48 | + | withShipped, | |
| 46 | 49 | } from "./home.ts"; | |
| 47 | 50 | ||
| 48 | 51 | const HOUR = 3_600_000; | |
| ⋯ | |||
| 780 | 783 | }); | |
| 781 | 784 | assert.deepEqual({ ...away, settled: away.settled.length }, { settled: 2, shipped: 1, waiting: 1, deploying: 0, failed: 0, needs: 3 }); | |
| 782 | 785 | }); | |
| 786 | + | ||
| 787 | + | test("Summarize asks about the span the page shows, from its start in the viewer's zone", () => { | |
| 788 | + | const from = Date.UTC(2026, 9, 13, 18, 0); | |
| 789 | + | const last = summarizeQuestion({ key: "last", note: null, words: "Since yesterday evening", from }, "UTC"); | |
| 790 | + | assert.equal(last, "Summarize what changed since I was last here (Oct 13, 6:00 PM) in plain English: what shipped, what's still deploying or failed, what's in flight, and what needs me."); | |
| 791 | + | assert.match(summarizeQuestion({ key: "24h", note: null, words: "In the last 24 hours", from }, "America/Chicago"), /in the last 24 hours \(since Oct 13, 1:00 PM\)/); | |
| 792 | + | // A first visit shows the last 24 hours, so it is not asked as "since I was last here". | |
| 793 | + | assert.doesNotMatch(summarizeQuestion({ key: "last", note: "first", words: "In the last 24 hours", from }, "UTC"), /last here/); | |
| 794 | + | }); | |
| 795 | + | ||
| 796 | + | test("Explain asks about the one change, by what it is", () => { | |
| 797 | + | assert.equal(explainQuestion({ kind: "merge", title: "Faster pushes", to: "/acme/web/pull/12" }), 'Explain "Faster pushes" (/acme/web/pull/12) in plain English: what changed, why, and is it live?'); | |
| 798 | + | assert.match(explainQuestion({ kind: "deploy", title: "web", to: "/acme/web/deployments/1" }), /went out in this deployment/); | |
| 799 | + | assert.match(explainQuestion({ kind: "release", title: "v1.2.0", to: "/acme/web/releases/v1.2.0" }), /What's new in v1\.2\.0/); | |
| 800 | + | }); | |
| 801 | + | ||
| 802 | + | test("Home's starters lead with what changed in the span", () => { | |
| 803 | + | const starters = homeStarters({ key: "7d", note: null, words: "In the last 7 days", from: 0 }, "UTC"); | |
| 804 | + | assert.equal(starters.length, 4); | |
| 805 | + | assert.equal(starters[0]!.label, "What changed in the last 7 days?"); | |
| 806 | + | assert.equal(homeStarters({ key: "last", note: null, words: "Since Tuesday", from: 0 }, "UTC")[0]!.label, "What changed since I was last here?"); | |
| 807 | + | }); | |
| 1352 | 1352 | if (dayIn(at, timeZone) === dayIn(now, timeZone)) return formatIn(at, timeZone, { hour: "numeric", minute: "2-digit" }); | |
| 1353 | 1353 | return now - at < 6 * DAY ? formatIn(at, timeZone, { weekday: "short", hour: "numeric" }) : shortDate(at, timeZone); | |
| 1354 | 1354 | } | |
| 1355 | + | ||
| 1356 | + | // --- Asking g1t from Home ------------------------------------------------------------ | |
| 1357 | + | ||
| 1358 | + | type AskedSpan = Pick<Span, "key" | "note" | "words" | "from">; | |
| 1359 | + | ||
| 1360 | + | /** The span as part of a question, with its start in the viewer's zone (g1t is told the zone): "since I was last here (Oct 14, 2:10 PM)". */ | |
| 1361 | + | function spanAsked(span: AskedSpan, timeZone: string | null): string { | |
| 1362 | + | const since = formatIn(span.from, timeZone, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }); | |
| 1363 | + | if (span.key === "last" && span.note == null) return `since I was last here (${since})`; | |
| 1364 | + | return `${span.words.charAt(0).toLowerCase()}${span.words.slice(1)} (since ${since})`; | |
| 1365 | + | } | |
| 1366 | + | ||
| 1367 | + | /** | |
| 1368 | + | * Home's Summarize: what changed in the span, in plain English, asked of | |
| 1369 | + | * g1t with the span's start, so the answer covers what the page shows. | |
| 1370 | + | */ | |
| 1371 | + | export function summarizeQuestion(span: AskedSpan, timeZone: string | null): string { | |
| 1372 | + | return `Summarize what changed ${spanAsked(span, timeZone)} in plain English: what shipped, what's still deploying or failed, what's in flight, and what needs me.`; | |
| 1373 | + | } | |
| 1374 | + | ||
| 1375 | + | /** A Landed row's Explain: what one change did and why, in plain English. */ | |
| 1376 | + | export function explainQuestion(change: Pick<LandedChange, "kind" | "title" | "to">): string { | |
| 1377 | + | if (change.kind === "deploy") return `What went out in this deployment (${change.to}), in plain English?`; | |
| 1378 | + | if (change.kind === "release" || change.kind === "package") return `What's new in ${change.title} (${change.to}), in plain English?`; | |
| 1379 | + | return `Explain "${change.title}" (${change.to}) in plain English: what changed, why, and is it live?`; | |
| 1380 | + | } | |
| 1381 | + | ||
| 1382 | + | /** What Home's Ask box offers to ask first: a label, and the question it asks. */ | |
| 1383 | + | export function homeStarters(span: AskedSpan, timeZone: string | null): { label: string; question: string }[] { | |
| 1384 | + | const changed = span.key === "last" && span.note == null ? "since I was last here" : `${span.words.charAt(0).toLowerCase()}${span.words.slice(1)}`; | |
| 1385 | + | return [ | |
| 1386 | + | { label: `What changed ${changed}?`, question: summarizeQuestion(span, timeZone) }, | |
| 1387 | + | { label: "What needs me first?", question: "What needs me first? Most urgent first, one line each on what to do." }, | |
| 1388 | + | { label: "What shipped today?", question: "What shipped today? What's live, what's still deploying, and what failed." }, | |
| 1389 | + | { label: "Who's working on what?", question: "Who's working on what right now, and is anything stuck?" }, | |
| 1390 | + | ]; | |
| 1391 | + | } |
| 19 | 19 | StartHere, | |
| 20 | 20 | WindowSwitch, | |
| 21 | 21 | } from "../../components/home"; | |
| 22 | + | import { HomeAsk, SummarizeButton } from "../../components/home-ask"; | |
| 22 | 23 | import { Skeleton } from "../../components/ui/skeleton"; | |
| 23 | 24 | import { sidebarOrNull } from "../../lib/chat.server"; | |
| 24 | 25 | import { | |
| ⋯ | |||
| 81 | 82 | const NOTIFICATIONS_READ = 50; | |
| 82 | 83 | ||
| 83 | 84 | /** | |
| 84 | − | * Home: the workspace's front page. Where you're needed now, with no time | |
| 85 | + | * Home: the workspace's front page. First a box that asks g1t | |
| 86 | + | * (components/home-ask: Summarize on the span and Explain on each change | |
| 87 | + | * that landed ask it too); then where you're needed now, with no time | |
| 85 | 88 | * window; what happened since you were last here (or over the last 24 | |
| 86 | 89 | * hours or 7 days, `?window=`): what people did, from the events | |
| 87 | 90 | * service's digest of the span and chat's, then what landed; what is | |
| ⋯ | |||
| 242 | 245 | return ( | |
| 243 | 246 | <div className="mx-auto w-full max-w-260 space-y-5 px-4 py-5 md:px-10 md:py-8"> | |
| 244 | 247 | <OnboardingOnPage slug={slug} view={loaderData.onboarding} asked={loaderData.setupAsked} /> | |
| 245 | − | <header className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between"> | |
| 246 | − | <div className="min-w-0"> | |
| 247 | − | <h1 className="text-[1.75rem] leading-tight font-semibold tracking-tight">{span.key === "last" && span.note == null ? "Welcome back" : "Home"}</h1> | |
| 248 | − | <p className="mt-1.5 max-w-2xl text-sm leading-relaxed text-muted"> | |
| 249 | − | <Suspense fallback={<Skeleton className="inline-block h-3.5 w-72 align-middle" />}> | |
| 250 | − | <Await resolve={head}> | |
| 251 | − | {([digest, happened, att]) => ( | |
| 252 | − | <> | |
| 253 | − | <span className="text-fg-soft">{waitingSentence(att.rows.length)}</span> {spanSentence(span, digest, happened?.landed?.length ?? null)} | |
| 254 | − | </> | |
| 255 | − | )} | |
| 256 | − | </Await> | |
| 257 | − | </Suspense> | |
| 258 | − | </p> | |
| 259 | − | </div> | |
| 260 | − | <WindowSwitch span={span} /> | |
| 248 | + | {/* 0. Ask g1t anything: the answer opens in the top bar's Ask panel. */} | |
| 249 | + | <header className="space-y-3"> | |
| 250 | + | <HomeAsk span={span} tz={tz} /> | |
| 251 | + | <p className="mx-auto max-w-2xl text-center text-sm leading-relaxed text-muted"> | |
| 252 | + | <Suspense fallback={<Skeleton className="inline-block h-3.5 w-72 align-middle" />}> | |
| 253 | + | <Await resolve={head}> | |
| 254 | + | {([digest, happened, att]) => ( | |
| 255 | + | <> | |
| 256 | + | <span className="text-fg-soft">{waitingSentence(att.rows.length)}</span> {spanSentence(span, digest, happened?.landed?.length ?? null)} | |
| 257 | + | </> | |
| 258 | + | )} | |
| 259 | + | </Await> | |
| 260 | + | </Suspense> | |
| 261 | + | </p> | |
| 261 | 262 | </header> | |
| 262 | 263 | ||
| 263 | 264 | {/* 1. Where you're needed now: no window, first and biggest. Start here beside it on wide screens, under it on phones. */} | |
| ⋯ | |||
| 288 | 289 | ||
| 289 | 290 | {/* 2. What happened in the span. */} | |
| 290 | 291 | <section aria-labelledby="since" className="space-y-4"> | |
| 291 | − | <div className="flex flex-wrap items-baseline gap-x-3 gap-y-1 pt-2"> | |
| 292 | − | <h2 id="since" className="text-lg font-semibold tracking-tight text-fg"> | |
| 293 | − | {span.key === "last" && span.note == null ? "Since you were last here" : span.words} | |
| 294 | − | </h2> | |
| 295 | − | <p className="text-sm text-muted"> | |
| 296 | − | {span.key === "last" && span.note == null ? `${span.words} · ${span.length}` : spanNote(span.note)} | |
| 297 | − | </p> | |
| 292 | + | <div className="flex flex-wrap items-center gap-x-3 gap-y-2 pt-2"> | |
| 293 | + | <div className="flex min-w-0 flex-wrap items-baseline gap-x-3 gap-y-1"> | |
| 294 | + | <h2 id="since" className="text-lg font-semibold tracking-tight text-fg"> | |
| 295 | + | {span.key === "last" && span.note == null ? "Since you were last here" : span.words} | |
| 296 | + | </h2> | |
| 297 | + | <p className="text-sm text-muted"> | |
| 298 | + | {span.key === "last" && span.note == null ? `${span.words} · ${span.length}` : spanNote(span.note)} | |
| 299 | + | </p> | |
| 300 | + | </div> | |
| 301 | + | <div className="flex flex-wrap items-center gap-2 sm:ml-auto"> | |
| 302 | + | <SummarizeButton span={span} tz={tz} /> | |
| 303 | + | <WindowSwitch span={span} /> | |
| 304 | + | </div> | |
| 298 | 305 | </div> | |
| 299 | 306 | <Suspense | |
| 300 | 307 | fallback={ | |
Comments
Sign in to comment.