mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-08 02:27:32 +00:00
Imported from andrewyng/aisuite@1b4bbf303e (contents of its platform/ directory, hoisted to the repo root). Development history prior to this commit lives in that repository. Co-authored-by: Devika <devikaverma11@gmail.com>
133 lines
5.6 KiB
TypeScript
133 lines
5.6 KiB
TypeScript
import { useEffect, useState } from "react";
|
||
import { getConnectors, getSessionConnections } from "../api";
|
||
import type { Attachment } from "../types";
|
||
import { ConnectorIcon } from "../connectors/ConnectorIcon";
|
||
import { indexConnectors, visualFor, type ConnectorMap } from "../connectors/visuals";
|
||
import { useRoots } from "../useRoots";
|
||
import { AddFolderForm } from "./AddFolderForm";
|
||
|
||
// Empty-state for a fresh Cowork session (§27): a greeting, exactly three concrete template
|
||
// tasks, and the composer — nothing else. Each task carries its own setup: no icon tiles (the
|
||
// title is the row), connector dots on the sub-line (brand color = connected and enabled for
|
||
// this session, grayscale = not — §23's vocabulary), and sub-line copy that is always the task's
|
||
// OUTCOME, never connection state. Sources ready → "Start →" on hover, click prefills the
|
||
// composer. Not ready → "Configure ›" always visible (for a gated row the setup action IS the
|
||
// row's meaning), opening the §23 Session settings drawer — no second setup surface here.
|
||
|
||
const FOLDER_PROMPT = "Analyze the files in this folder and summarize what matters.";
|
||
const HUBSPOT_PROMPT =
|
||
"Create a report on my recent HubSpot leads: sources, stages, and who needs follow-up.";
|
||
const GH_SLACK_PROMPT =
|
||
"Set up a weekly progress report: summarize activity in my GitHub repos and post it to Slack every Friday morning.";
|
||
|
||
export function SessionIntro({
|
||
sessionId,
|
||
onOpenSessionSettings,
|
||
onPrefill,
|
||
}: {
|
||
sessionId: string;
|
||
// Opens the §23 Session settings drawer (sources section) — the gated rows' Configure target.
|
||
onOpenSessionSettings: () => void;
|
||
onPrefill: (text: string, attachments?: Attachment[]) => void;
|
||
}) {
|
||
const { roots, busy, error, addRoot } = useRoots(sessionId);
|
||
const [live, setLive] = useState<Set<string>>(new Set());
|
||
const [byName, setByName] = useState<ConnectorMap>({});
|
||
const [addingFolder, setAddingFolder] = useState(false);
|
||
|
||
useEffect(() => {
|
||
// Live = what this session can touch right now (connected AND not muted here) — the same
|
||
// truth the §23 glance renders, so the dots here can never disagree with the row above.
|
||
getSessionConnections(sessionId)
|
||
.then((c) => setLive(new Set(c.connected.filter((x) => x.enabled).map((x) => x.connector))))
|
||
.catch(() => {});
|
||
getConnectors()
|
||
.then((list) => setByName(indexConnectors(list)))
|
||
.catch(() => {});
|
||
}, [sessionId]);
|
||
|
||
const shared = roots.filter((r) => !r.primary);
|
||
const hubspotReady = live.has("hubspot");
|
||
const ghSlackReady = live.has("github") && live.has("slack");
|
||
|
||
const dot = (name: string, on: boolean) => (
|
||
<span className={"task-dot" + (on ? "" : " off")} key={name}>
|
||
<ConnectorIcon connector={visualFor(name, "connector", byName)} size={12} />
|
||
</span>
|
||
);
|
||
|
||
const pickFolder = () => {
|
||
// A shared folder already exists → straight to the prompt; otherwise share one first.
|
||
if (shared.length > 0) onPrefill(FOLDER_PROMPT);
|
||
else setAddingFolder((v) => !v);
|
||
};
|
||
|
||
return (
|
||
<div className="intro">
|
||
<h1 className="greeting">
|
||
<span className="mark">✦</span> What should we produce?
|
||
</h1>
|
||
<p className="intro-lede">
|
||
Pick a task to start — I'll do the work and save the result. Or just type what you need
|
||
below.
|
||
</p>
|
||
|
||
<div className="intro-tasks">
|
||
<button className="task-card" data-testid="intro-task-folder" onClick={pickFolder}>
|
||
<span className="task-card-body">
|
||
<span className="task-card-title">Analyze the files in a directory</span>
|
||
<span className="task-card-sub">I'll read them and summarize what matters</span>
|
||
</span>
|
||
<span className="task-card-act">Pick a folder →</span>
|
||
</button>
|
||
{addingFolder && (
|
||
<div className="intro-addfolder">
|
||
<AddFolderForm
|
||
startOpen
|
||
busy={busy}
|
||
onAdd={async (path, writable) => {
|
||
const ok = await addRoot(path, writable);
|
||
if (ok !== false) onPrefill(FOLDER_PROMPT);
|
||
return ok;
|
||
}}
|
||
onDismiss={() => setAddingFolder(false)}
|
||
/>
|
||
{error && <div className="roots-err">{error}</div>}
|
||
</div>
|
||
)}
|
||
|
||
<button
|
||
className={"task-card" + (hubspotReady ? "" : " gated")}
|
||
data-testid="intro-task-hubspot"
|
||
onClick={() => (hubspotReady ? onPrefill(HUBSPOT_PROMPT) : onOpenSessionSettings())}
|
||
>
|
||
<span className="task-card-body">
|
||
<span className="task-card-title">Create a report from my HubSpot leads</span>
|
||
<span className="task-card-sub">
|
||
{dot("hubspot", hubspotReady)}
|
||
Sources, stages, and who needs follow-up
|
||
</span>
|
||
</span>
|
||
<span className="task-card-act">{hubspotReady ? "Start →" : "Configure ›"}</span>
|
||
</button>
|
||
|
||
<button
|
||
className={"task-card" + (ghSlackReady ? "" : " gated")}
|
||
data-testid="intro-task-github-slack"
|
||
onClick={() => (ghSlackReady ? onPrefill(GH_SLACK_PROMPT) : onOpenSessionSettings())}
|
||
>
|
||
<span className="task-card-body">
|
||
<span className="task-card-title">Automate a weekly GitHub progress report to Slack</span>
|
||
<span className="task-card-sub">
|
||
{dot("github", live.has("github"))}
|
||
{dot("slack", live.has("slack"))}
|
||
Repo activity, summarized and posted every Friday
|
||
</span>
|
||
</span>
|
||
<span className="task-card-act">{ghSlackReady ? "Start →" : "Configure ›"}</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|