@opengeni/react 5.0.5-canary.0 → 5.0.5-canary.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-SCZ6SUBW.js → chunk-H63SQTGI.js} +1258 -1837
- package/dist/chunk-H63SQTGI.js.map +1 -0
- package/dist/chunk-HQV2I5HV.js +124 -0
- package/dist/chunk-HQV2I5HV.js.map +1 -0
- package/dist/chunk-M2HNWZYR.js +835 -0
- package/dist/chunk-M2HNWZYR.js.map +1 -0
- package/dist/{chunk-Z7WBDB4E.js → chunk-V3WX3TQG.js} +283 -391
- package/dist/chunk-V3WX3TQG.js.map +1 -0
- package/dist/components/message-timeline.d.ts +3 -1
- package/dist/composer.js +2 -1
- package/dist/connect-chooser.d.ts +1 -0
- package/dist/connect-panel.d.ts +2 -1
- package/dist/connect.d.ts +7 -0
- package/dist/connect.js +791 -183
- package/dist/connect.js.map +1 -1
- package/dist/connection-catalog.d.ts +33 -0
- package/dist/connection-installed.d.ts +15 -0
- package/dist/connection-logo.d.ts +6 -0
- package/dist/connection-type-picker.d.ts +6 -0
- package/dist/index.js +17 -12
- package/dist/index.js.map +1 -1
- package/dist/plugin-details.d.ts +14 -0
- package/dist/plugin-discovery.d.ts +14 -0
- package/dist/session-ui.d.ts +3 -0
- package/dist/session-ui.js +113 -2
- package/dist/session-ui.js.map +1 -1
- package/dist/skill-discovery.d.ts +29 -0
- package/dist/timeline/activity-rail.d.ts +3 -1
- package/dist/timeline/genie-loading.d.ts +24 -0
- package/dist/timeline/startup-preference.d.ts +3 -0
- package/dist/timeline/startup-timings.d.ts +4 -0
- package/package.json +4 -3
- package/src/components/message-timeline.tsx +127 -20
- package/src/connect-chooser.tsx +81 -21
- package/src/connect-panel.tsx +7 -1
- package/src/connect.ts +21 -0
- package/src/connection-catalog.tsx +153 -0
- package/src/connection-installed.tsx +45 -0
- package/src/connection-logo.tsx +29 -0
- package/src/connection-type-picker.tsx +36 -0
- package/src/hooks/use-codex-accounts.ts +1 -0
- package/src/plugin-details.tsx +182 -0
- package/src/plugin-discovery.tsx +161 -0
- package/src/session-ui.ts +3 -0
- package/src/skill-discovery.tsx +152 -0
- package/src/timeline/activity-rail.tsx +75 -4
- package/src/timeline/genie-loading.tsx +112 -0
- package/src/timeline/startup-preference.ts +33 -0
- package/src/timeline/startup-timings.tsx +145 -0
- package/src/timeline/turn-summary.tsx +8 -3
- package/styles/compiled.css +73 -0
- package/styles/connect.css +149 -1
- package/styles/index.css +15 -0
- package/dist/chunk-SCZ6SUBW.js.map +0 -1
- package/dist/chunk-Z7WBDB4E.js.map +0 -1
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import type { PluginDiscoveryItem, PluginDiscoveryPage } from "@opengeni/sdk";
|
|
3
|
+
export type PluginDiscoveryProps = {
|
|
4
|
+
client: {
|
|
5
|
+
discoverPlugins(
|
|
6
|
+
workspaceId: string,
|
|
7
|
+
options?: { query?: string; provider?: string; offset?: number },
|
|
8
|
+
): Promise<PluginDiscoveryPage>;
|
|
9
|
+
};
|
|
10
|
+
workspaceId: string;
|
|
11
|
+
query: string;
|
|
12
|
+
onOpen: (item: PluginDiscoveryItem) => void;
|
|
13
|
+
};
|
|
14
|
+
export function PluginDiscovery(props: PluginDiscoveryProps) {
|
|
15
|
+
const [provider, setProvider] = useState("");
|
|
16
|
+
return (
|
|
17
|
+
<section className="og-plugin-discovery" aria-label="Discover plugins">
|
|
18
|
+
<header>
|
|
19
|
+
<h3>Browse plugins</h3>
|
|
20
|
+
<div className="og-plugin-filters" role="group" aria-label="Plugin registry">
|
|
21
|
+
{[
|
|
22
|
+
{ value: "", label: "All" },
|
|
23
|
+
{ value: "openai", label: "OpenAI plugin registry" },
|
|
24
|
+
{ value: "anthropic", label: "Anthropic plugin registry" },
|
|
25
|
+
].map((option) => (
|
|
26
|
+
<button
|
|
27
|
+
key={option.value}
|
|
28
|
+
type="button"
|
|
29
|
+
aria-pressed={provider === option.value}
|
|
30
|
+
onClick={() => setProvider(option.value)}
|
|
31
|
+
>
|
|
32
|
+
{option.label}
|
|
33
|
+
</button>
|
|
34
|
+
))}
|
|
35
|
+
</div>
|
|
36
|
+
</header>
|
|
37
|
+
<Results
|
|
38
|
+
key={`${props.workspaceId}:${props.query}:${provider}`}
|
|
39
|
+
{...props}
|
|
40
|
+
provider={provider}
|
|
41
|
+
/>
|
|
42
|
+
</section>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
function Results({
|
|
46
|
+
client,
|
|
47
|
+
workspaceId,
|
|
48
|
+
query,
|
|
49
|
+
provider,
|
|
50
|
+
onOpen,
|
|
51
|
+
}: PluginDiscoveryProps & { provider: string }) {
|
|
52
|
+
const [items, setItems] = useState<PluginDiscoveryItem[]>([]);
|
|
53
|
+
const [offset, setOffset] = useState(0);
|
|
54
|
+
const [next, setNext] = useState<number | null>(null);
|
|
55
|
+
const [total, setTotal] = useState(0);
|
|
56
|
+
const [loading, setLoading] = useState(true);
|
|
57
|
+
const [error, setError] = useState(false);
|
|
58
|
+
const [retry, setRetry] = useState(0);
|
|
59
|
+
const sentinel = useRef<HTMLDivElement>(null);
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
let active = true;
|
|
62
|
+
setLoading(true);
|
|
63
|
+
setError(false);
|
|
64
|
+
const timer = setTimeout(
|
|
65
|
+
() => {
|
|
66
|
+
void client.discoverPlugins(workspaceId, { query, provider, offset }).then(
|
|
67
|
+
(page) => {
|
|
68
|
+
if (!active) return;
|
|
69
|
+
setItems((previous) => (offset ? [...previous, ...page.items] : page.items));
|
|
70
|
+
setTotal(page.total);
|
|
71
|
+
setNext(page.nextOffset);
|
|
72
|
+
setLoading(false);
|
|
73
|
+
},
|
|
74
|
+
() => {
|
|
75
|
+
if (active) {
|
|
76
|
+
setError(true);
|
|
77
|
+
setLoading(false);
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
},
|
|
82
|
+
offset ? 0 : 200,
|
|
83
|
+
);
|
|
84
|
+
return () => {
|
|
85
|
+
active = false;
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
};
|
|
88
|
+
}, [client, workspaceId, query, provider, offset, retry]);
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (loading || error || next === null || !sentinel.current) return;
|
|
91
|
+
const observer = new IntersectionObserver(
|
|
92
|
+
(entries) => {
|
|
93
|
+
if (entries.some((entry) => entry.isIntersecting)) setOffset(next);
|
|
94
|
+
},
|
|
95
|
+
{ rootMargin: "200px" },
|
|
96
|
+
);
|
|
97
|
+
observer.observe(sentinel.current);
|
|
98
|
+
return () => observer.disconnect();
|
|
99
|
+
}, [loading, error, next]);
|
|
100
|
+
return (
|
|
101
|
+
<>
|
|
102
|
+
{!loading && !error ? (
|
|
103
|
+
<p role="status">
|
|
104
|
+
{total ? `${total} plugins` : "No matching plugins. Try another search or registry."}
|
|
105
|
+
</p>
|
|
106
|
+
) : null}
|
|
107
|
+
<div className="og-plugin-discovery-grid">
|
|
108
|
+
{items.map((item) => (
|
|
109
|
+
<button
|
|
110
|
+
key={item.id}
|
|
111
|
+
className="og-plugin-discovery-row"
|
|
112
|
+
type="button"
|
|
113
|
+
onClick={() => onOpen(item)}
|
|
114
|
+
>
|
|
115
|
+
{item.logoUrl ? (
|
|
116
|
+
<img
|
|
117
|
+
src={item.logoUrl}
|
|
118
|
+
alt=""
|
|
119
|
+
loading="lazy"
|
|
120
|
+
onError={(event) => {
|
|
121
|
+
event.currentTarget.style.display = "none";
|
|
122
|
+
}}
|
|
123
|
+
/>
|
|
124
|
+
) : null}
|
|
125
|
+
<span className="og-plugin-copy">
|
|
126
|
+
<span className="og-plugin-title">
|
|
127
|
+
<strong>{item.displayName.replace(/-/g, " ")}</strong>
|
|
128
|
+
<small>
|
|
129
|
+
{item.provider === "openai"
|
|
130
|
+
? "OpenAI plugin registry"
|
|
131
|
+
: "Anthropic plugin registry"}
|
|
132
|
+
</small>
|
|
133
|
+
</span>
|
|
134
|
+
<span className="og-plugin-description">{item.description}</span>
|
|
135
|
+
{item.category ? <span className="og-plugin-category">{item.category}</span> : null}
|
|
136
|
+
</span>
|
|
137
|
+
<span className="og-plugin-open" aria-hidden="true">
|
|
138
|
+
›
|
|
139
|
+
</span>
|
|
140
|
+
</button>
|
|
141
|
+
))}
|
|
142
|
+
</div>
|
|
143
|
+
{loading ? <p role="status">Loading plugins…</p> : null}
|
|
144
|
+
{error ? (
|
|
145
|
+
<p role="alert">
|
|
146
|
+
Couldn’t load plugins.{" "}
|
|
147
|
+
<button type="button" onClick={() => setRetry((value) => value + 1)}>
|
|
148
|
+
Retry
|
|
149
|
+
</button>
|
|
150
|
+
</p>
|
|
151
|
+
) : null}
|
|
152
|
+
<div ref={sentinel}>
|
|
153
|
+
{next !== null && !loading && !error ? (
|
|
154
|
+
<button className="og-plugin-more" type="button" onClick={() => setOffset(next)}>
|
|
155
|
+
Show more plugins
|
|
156
|
+
</button>
|
|
157
|
+
) : null}
|
|
158
|
+
</div>
|
|
159
|
+
</>
|
|
160
|
+
);
|
|
161
|
+
}
|
package/src/session-ui.ts
CHANGED
|
@@ -43,3 +43,6 @@ export type {
|
|
|
43
43
|
SessionChromeSignalTone,
|
|
44
44
|
} from "./components/session-chrome";
|
|
45
45
|
export { SessionCommandsPanel } from "./components/session-commands-panel";
|
|
46
|
+
export { StartupTimings } from "./timeline/startup-timings";
|
|
47
|
+
export { useStartupDetails, setStartupDetails } from "./timeline/startup-preference";
|
|
48
|
+
export type { GenieLoadingOptions } from "./timeline/genie-loading";
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export type SkillDiscoveryItem = {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
source: string;
|
|
7
|
+
installs: number;
|
|
8
|
+
url: string;
|
|
9
|
+
};
|
|
10
|
+
export type SkillDiscoveryPage = { items: SkillDiscoveryItem[]; nextCursor: null };
|
|
11
|
+
export type SkillDiscoveryClient = {
|
|
12
|
+
searchPublicSkills(workspaceId: string, query: string): Promise<SkillDiscoveryPage>;
|
|
13
|
+
};
|
|
14
|
+
export type SkillDiscoveryProps = {
|
|
15
|
+
installedSkills?: readonly { name: string; repositoryUrl: string; sourceUrl: string }[];
|
|
16
|
+
client: SkillDiscoveryClient;
|
|
17
|
+
workspaceId: string;
|
|
18
|
+
query: string;
|
|
19
|
+
onImport: (url: string) => void;
|
|
20
|
+
canManage: boolean;
|
|
21
|
+
onSearch?: (() => void) | undefined;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Host-owned query and import flow; browse presentation is reusable independently. */
|
|
25
|
+
export function SkillDiscovery(props: SkillDiscoveryProps) {
|
|
26
|
+
const query = props.query.trim();
|
|
27
|
+
return (
|
|
28
|
+
<section className="og-skill-discovery" aria-label="Discover skills">
|
|
29
|
+
<header>
|
|
30
|
+
<h3>Discover skills</h3>
|
|
31
|
+
<a
|
|
32
|
+
href="https://skills.sh/"
|
|
33
|
+
target="_blank"
|
|
34
|
+
rel="noopener noreferrer"
|
|
35
|
+
className="og-skill-discovery-browse"
|
|
36
|
+
>
|
|
37
|
+
Browse skills.sh ↗
|
|
38
|
+
</a>
|
|
39
|
+
</header>
|
|
40
|
+
{!query && props.onSearch ? (
|
|
41
|
+
<button type="button" className="og-skill-discovery-search" onClick={props.onSearch}>
|
|
42
|
+
Search skills
|
|
43
|
+
</button>
|
|
44
|
+
) : null}
|
|
45
|
+
<DiscoveryResults key={`${props.workspaceId}:${query}`} {...props} query={query} />
|
|
46
|
+
</section>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const EMPTY_INSTALLED_SKILLS: NonNullable<SkillDiscoveryProps["installedSkills"]> = [];
|
|
51
|
+
|
|
52
|
+
function DiscoveryResults({
|
|
53
|
+
client,
|
|
54
|
+
workspaceId,
|
|
55
|
+
query,
|
|
56
|
+
canManage,
|
|
57
|
+
onImport,
|
|
58
|
+
installedSkills = EMPTY_INSTALLED_SKILLS,
|
|
59
|
+
}: SkillDiscoveryProps) {
|
|
60
|
+
const [result, setResult] = useState<SkillDiscoveryPage | null>(null);
|
|
61
|
+
const [loading, setLoading] = useState(query.length >= 2);
|
|
62
|
+
const [error, setError] = useState(false);
|
|
63
|
+
const [retry, setRetry] = useState(0);
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
let active = true;
|
|
66
|
+
setError(false);
|
|
67
|
+
if (query.length < 2) return;
|
|
68
|
+
setLoading(true);
|
|
69
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
70
|
+
const search = async (attempt: number) => {
|
|
71
|
+
try {
|
|
72
|
+
const data = await client.searchPublicSkills(workspaceId, query);
|
|
73
|
+
if (!active) return;
|
|
74
|
+
setResult(data);
|
|
75
|
+
setLoading(false);
|
|
76
|
+
} catch (cause) {
|
|
77
|
+
if (!active) return;
|
|
78
|
+
const status =
|
|
79
|
+
typeof cause === "object" && cause !== null && "status" in cause
|
|
80
|
+
? cause.status
|
|
81
|
+
: undefined;
|
|
82
|
+
const transient =
|
|
83
|
+
cause instanceof TypeError ||
|
|
84
|
+
(typeof status === "number" && status >= 500 && status <= 599);
|
|
85
|
+
if (attempt === 0 && transient) {
|
|
86
|
+
timer = setTimeout(() => void search(1), 750);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
setError(true);
|
|
90
|
+
setLoading(false);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
timer = setTimeout(() => void search(0), 300);
|
|
94
|
+
return () => {
|
|
95
|
+
active = false;
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
};
|
|
98
|
+
}, [client, workspaceId, query, retry]);
|
|
99
|
+
return (
|
|
100
|
+
<div aria-busy={loading}>
|
|
101
|
+
<div className="og-skill-discovery-grid">
|
|
102
|
+
{result?.items.map((skill) => {
|
|
103
|
+
const installed = installedSkills.some(
|
|
104
|
+
(entry) =>
|
|
105
|
+
entry.sourceUrl.replace(/\/$/, "").toLowerCase() === skill.url.toLowerCase() ||
|
|
106
|
+
(entry.repositoryUrl.replace(/\/$/, "").toLowerCase() ===
|
|
107
|
+
`https://github.com/${skill.source}`.toLowerCase() &&
|
|
108
|
+
entry.name.toLowerCase() === skill.name.toLowerCase()),
|
|
109
|
+
);
|
|
110
|
+
return (
|
|
111
|
+
<button
|
|
112
|
+
key={skill.id}
|
|
113
|
+
type="button"
|
|
114
|
+
className="og-skill-discovery-item"
|
|
115
|
+
disabled={!canManage}
|
|
116
|
+
onClick={() => onImport(skill.url)}
|
|
117
|
+
>
|
|
118
|
+
<span className="og-skill-discovery-copy">
|
|
119
|
+
<strong>{skill.name}</strong>
|
|
120
|
+
<span>{skill.source}</span>
|
|
121
|
+
<small>{skill.installs.toLocaleString()} installs</small>
|
|
122
|
+
</span>
|
|
123
|
+
<span className="og-skill-discovery-action">
|
|
124
|
+
{installed ? (
|
|
125
|
+
<>
|
|
126
|
+
<span aria-hidden="true">✓ </span>Installed
|
|
127
|
+
</>
|
|
128
|
+
) : !canManage ? (
|
|
129
|
+
"Admin required"
|
|
130
|
+
) : (
|
|
131
|
+
"Preview →"
|
|
132
|
+
)}
|
|
133
|
+
</span>
|
|
134
|
+
</button>
|
|
135
|
+
);
|
|
136
|
+
})}
|
|
137
|
+
</div>
|
|
138
|
+
{loading ? <p role="status">Loading skills…</p> : null}
|
|
139
|
+
{error ? (
|
|
140
|
+
<div role="alert" className="og-skill-discovery-error">
|
|
141
|
+
Could not load skills.
|
|
142
|
+
<button type="button" onClick={() => setRetry((value) => value + 1)}>
|
|
143
|
+
Retry
|
|
144
|
+
</button>
|
|
145
|
+
</div>
|
|
146
|
+
) : null}
|
|
147
|
+
{result && !result.items.length && !loading && !error ? (
|
|
148
|
+
<p role="status">No skills found. Try a different search.</p>
|
|
149
|
+
) : null}
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
|
2
|
+
import { GenieLoading } from "./genie-loading";
|
|
3
|
+
import { useStartupDetails } from "./startup-preference";
|
|
1
4
|
import { ArrowRightIcon, BotIcon, BrainCircuitIcon } from "lucide-react";
|
|
2
|
-
import { lazy, Suspense, useLayoutEffect, useRef } from "react";
|
|
5
|
+
import { lazy, Suspense, useLayoutEffect, useRef, useState } from "react";
|
|
3
6
|
import { jsx as rowJsx, jsxs as rowJsxs } from "react/jsx-runtime";
|
|
4
7
|
import { Markdown } from "../components/markdown";
|
|
5
8
|
import { cn } from "../lib/cn";
|
|
@@ -28,6 +31,8 @@ const LazyPlatformActivityRow = lazy(() => import("./platform-activity-row"));
|
|
|
28
31
|
|
|
29
32
|
export type ActivityRailProps = {
|
|
30
33
|
items: ActivityItem[];
|
|
34
|
+
/** The owning turn remains active between individual phase receipts. */
|
|
35
|
+
startupActive?: boolean;
|
|
31
36
|
/** Renderer registry for tool calls. Defaults to {@link defaultToolRegistry}. */
|
|
32
37
|
toolRegistry?: ToolRegistry | undefined;
|
|
33
38
|
/** Drill into a spawned worker session. */
|
|
@@ -60,6 +65,7 @@ function familyOf(item: ActivityItem): string {
|
|
|
60
65
|
|
|
61
66
|
export function ActivityRail({
|
|
62
67
|
items,
|
|
68
|
+
startupActive,
|
|
63
69
|
toolRegistry = defaultToolRegistry,
|
|
64
70
|
onOpenSession,
|
|
65
71
|
onMemoryClick,
|
|
@@ -68,6 +74,37 @@ export function ActivityRail({
|
|
|
68
74
|
bare,
|
|
69
75
|
className,
|
|
70
76
|
}: ActivityRailProps) {
|
|
77
|
+
const debug = useStartupDetails();
|
|
78
|
+
const reducedMotion = useReducedMotion();
|
|
79
|
+
const [detailsOpen, setDetailsOpen] = useState(false);
|
|
80
|
+
const phases = items.filter((item) => item.kind === "startup-phase");
|
|
81
|
+
// Empty reasoning envelopes can arrive before any visible model output.
|
|
82
|
+
const hasWork = items.some(
|
|
83
|
+
(item) =>
|
|
84
|
+
item.kind !== "startup-phase" && (item.kind !== "reasoning" || item.text.trim().length > 0),
|
|
85
|
+
);
|
|
86
|
+
const interrupted = phases.some(
|
|
87
|
+
(item) => item.status === "failed" || item.status === "cancelled",
|
|
88
|
+
);
|
|
89
|
+
const providerResponded = phases.some(
|
|
90
|
+
(item) => item.phase === "provider_first_byte" && item.status === "complete",
|
|
91
|
+
);
|
|
92
|
+
const preparing =
|
|
93
|
+
!hasWork &&
|
|
94
|
+
!interrupted &&
|
|
95
|
+
(startupActive ?? (!providerResponded && phases.some((item) => item.status === "running")));
|
|
96
|
+
const visibleItems =
|
|
97
|
+
debug || detailsOpen
|
|
98
|
+
? items
|
|
99
|
+
: items.filter((item) =>
|
|
100
|
+
item.kind === "startup-phase"
|
|
101
|
+
? item.status === "failed" || item.status === "cancelled"
|
|
102
|
+
: item.kind !== "reasoning" || item.text.trim().length > 0,
|
|
103
|
+
);
|
|
104
|
+
const startedAt = phases.reduce(
|
|
105
|
+
(first, item) => (item.startedAt < first ? item.startedAt : first),
|
|
106
|
+
phases[0]?.startedAt ?? "",
|
|
107
|
+
);
|
|
71
108
|
const enterMounted = useEntranceAnimation();
|
|
72
109
|
// Live gate: rails born during bulk capture enter=false forever; with a
|
|
73
110
|
// seen-id map we still want later live appends to fade (ids gate remounts).
|
|
@@ -102,7 +139,7 @@ export function ActivityRail({
|
|
|
102
139
|
// Rows sit TIGHT by default (gap-0.5) so a same-family run reads as one
|
|
103
140
|
// calm cluster; a family change opens real breathing room (mt-3) below,
|
|
104
141
|
// so a long rail reads as a few clusters, not a metronome of rows.
|
|
105
|
-
"flex flex-col gap-0.5",
|
|
142
|
+
"relative flex flex-col gap-0.5",
|
|
106
143
|
!bare && "border-l-2 border-og-border pl-3 sm:pl-4",
|
|
107
144
|
// Whole-rail enter: standalone rails only (no seen-id map). Inside
|
|
108
145
|
// MessageTimeline, unknown ids take per-row enter — remounts stay quiet.
|
|
@@ -110,8 +147,42 @@ export function ActivityRail({
|
|
|
110
147
|
className,
|
|
111
148
|
)}
|
|
112
149
|
>
|
|
113
|
-
|
|
114
|
-
|
|
150
|
+
<AnimatePresence initial={false}>
|
|
151
|
+
{preparing && !debug ? (
|
|
152
|
+
<motion.div
|
|
153
|
+
key="startup"
|
|
154
|
+
initial={{ opacity: 0 }}
|
|
155
|
+
animate={{ opacity: 1, height: "auto" }}
|
|
156
|
+
exit={{
|
|
157
|
+
opacity: 0,
|
|
158
|
+
height: 0,
|
|
159
|
+
pointerEvents: "none",
|
|
160
|
+
}}
|
|
161
|
+
transition={{
|
|
162
|
+
height: { duration: reducedMotion ? 0 : 0.32, ease: [0.22, 1, 0.36, 1] },
|
|
163
|
+
opacity: { duration: reducedMotion ? 0 : 0.16 },
|
|
164
|
+
}}
|
|
165
|
+
style={{ overflow: "hidden" }}
|
|
166
|
+
>
|
|
167
|
+
<GenieLoading
|
|
168
|
+
startedAt={startedAt}
|
|
169
|
+
detailsOpen={detailsOpen}
|
|
170
|
+
onShowDetails={() => setDetailsOpen((open) => !open)}
|
|
171
|
+
/>
|
|
172
|
+
</motion.div>
|
|
173
|
+
) : null}
|
|
174
|
+
</AnimatePresence>
|
|
175
|
+
{detailsOpen && !debug && !preparing ? (
|
|
176
|
+
<button
|
|
177
|
+
type="button"
|
|
178
|
+
className="og-genie-details self-start"
|
|
179
|
+
onClick={() => setDetailsOpen(false)}
|
|
180
|
+
>
|
|
181
|
+
Hide startup details
|
|
182
|
+
</button>
|
|
183
|
+
) : null}
|
|
184
|
+
{visibleItems.map((item, index) => {
|
|
185
|
+
const newFamily = index > 0 && familyOf(item) !== familyOf(visibleItems[index - 1]!);
|
|
115
186
|
const row = renderActivity(
|
|
116
187
|
item,
|
|
117
188
|
toolRegistry,
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { ThinkingOrb } from "thinking-orbs";
|
|
2
|
+
import { useThemeType } from "../lib/use-theme-type";
|
|
3
|
+
import { createContext, useContext, useEffect, useState, type ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
export type GenieLoadingRenderProps = {
|
|
6
|
+
startedAt: string;
|
|
7
|
+
detailsOpen: boolean;
|
|
8
|
+
onShowDetails: () => void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type GenieLoadingOptions = {
|
|
12
|
+
/** Replace the visual while preserving SDK loading visibility and transitions. */
|
|
13
|
+
render?: (props: GenieLoadingRenderProps) => ReactNode;
|
|
14
|
+
phrases?: readonly string[];
|
|
15
|
+
/** Public options stay independent of the renderer's declaration layout. */
|
|
16
|
+
orb?: {
|
|
17
|
+
state?:
|
|
18
|
+
| "working"
|
|
19
|
+
| "searching"
|
|
20
|
+
| "solving"
|
|
21
|
+
| "listening"
|
|
22
|
+
| "connecting"
|
|
23
|
+
| "weaving"
|
|
24
|
+
| "composing"
|
|
25
|
+
| "breathing"
|
|
26
|
+
| "shaping";
|
|
27
|
+
size?: 64 | 20;
|
|
28
|
+
speed?: number;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
export const GenieLoadingOptionsContext = createContext<GenieLoadingOptions | undefined>(undefined);
|
|
32
|
+
|
|
33
|
+
const PHRASES = [
|
|
34
|
+
"Polishing the lamp…",
|
|
35
|
+
"Consulting the carpet…",
|
|
36
|
+
"Untangling wishes…",
|
|
37
|
+
"Summoning a little cleverness…",
|
|
38
|
+
"Checking the fine print on infinity…",
|
|
39
|
+
"Warming up the abracadabra…",
|
|
40
|
+
"Rearranging the stars…",
|
|
41
|
+
"Negotiating with the lamp…",
|
|
42
|
+
"Dusting off a thousand years…",
|
|
43
|
+
"Wishful thinking…",
|
|
44
|
+
"Decanting a little magic…",
|
|
45
|
+
"Finding the good stardust…",
|
|
46
|
+
"Fluffing the magic carpet…",
|
|
47
|
+
"Putting a wish into motion…",
|
|
48
|
+
"A little hocus. A little pocus…",
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
/** Decorative copy never substitutes for a failure or claims measurable progress. */
|
|
52
|
+
export function GenieLoading({
|
|
53
|
+
startedAt,
|
|
54
|
+
onShowDetails,
|
|
55
|
+
detailsOpen = false,
|
|
56
|
+
}: {
|
|
57
|
+
startedAt: string;
|
|
58
|
+
onShowDetails: () => void;
|
|
59
|
+
detailsOpen?: boolean;
|
|
60
|
+
}) {
|
|
61
|
+
const options = useContext(GenieLoadingOptionsContext);
|
|
62
|
+
const phrases = options?.phrases?.length ? options.phrases : PHRASES;
|
|
63
|
+
const theme = useThemeType(undefined);
|
|
64
|
+
const [phrase, setPhrase] = useState(0);
|
|
65
|
+
const [showDetails, setShowDetails] = useState(false);
|
|
66
|
+
const [slow, setSlow] = useState(false);
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const update = () => {
|
|
69
|
+
setShowDetails(Date.now() - Date.parse(startedAt) >= 15_000);
|
|
70
|
+
setSlow(Date.now() - Date.parse(startedAt) >= 30_000);
|
|
71
|
+
if (!document.hidden) setPhrase(Math.floor(Math.random() * phrases.length));
|
|
72
|
+
};
|
|
73
|
+
update();
|
|
74
|
+
const timer = window.setInterval(update, 5_000);
|
|
75
|
+
return () => window.clearInterval(timer);
|
|
76
|
+
}, [startedAt, phrases]);
|
|
77
|
+
if (options?.render) return options.render({ startedAt, detailsOpen, onShowDetails });
|
|
78
|
+
return (
|
|
79
|
+
<div className="og-genie-loading">
|
|
80
|
+
<div
|
|
81
|
+
className="og-genie-orb"
|
|
82
|
+
aria-hidden="true"
|
|
83
|
+
style={{
|
|
84
|
+
width: options?.orb?.size ?? 64,
|
|
85
|
+
height: options?.orb?.size ?? 64,
|
|
86
|
+
flexBasis: options?.orb?.size ?? 64,
|
|
87
|
+
}}
|
|
88
|
+
>
|
|
89
|
+
<ThinkingOrb state="searching" size={64} theme={theme} speed={0.8} {...options?.orb} />
|
|
90
|
+
</div>
|
|
91
|
+
<div className="og-genie-copy">
|
|
92
|
+
<span className="sr-only" role="status">
|
|
93
|
+
{slow ? "Preparing your task. Taking longer than usual." : "Preparing your task."}
|
|
94
|
+
</span>
|
|
95
|
+
<span key={slow ? "slow" : phrase} className="og-genie-phrase" aria-hidden="true">
|
|
96
|
+
{slow ? "A little longer than usual…" : phrases[phrase % phrases.length]}
|
|
97
|
+
</span>
|
|
98
|
+
{showDetails || detailsOpen ? (
|
|
99
|
+
<button
|
|
100
|
+
type="button"
|
|
101
|
+
className="og-genie-details"
|
|
102
|
+
aria-expanded={detailsOpen}
|
|
103
|
+
onClick={onShowDetails}
|
|
104
|
+
>
|
|
105
|
+
{detailsOpen ? "Hide details" : "Behind the magic"}
|
|
106
|
+
<span aria-hidden="true"> ↗</span>
|
|
107
|
+
</button>
|
|
108
|
+
) : null}
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useSyncExternalStore } from "react";
|
|
2
|
+
|
|
3
|
+
const KEY = "opengeni:startup-details:v1";
|
|
4
|
+
const EVENT = "opengeni:startup-details-changed";
|
|
5
|
+
let fallback = false;
|
|
6
|
+
function snapshot() {
|
|
7
|
+
try {
|
|
8
|
+
return window.localStorage.getItem(KEY) === "true";
|
|
9
|
+
} catch {
|
|
10
|
+
return fallback;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function subscribe(listener: () => void) {
|
|
14
|
+
window.addEventListener(EVENT, listener);
|
|
15
|
+
window.addEventListener("storage", listener);
|
|
16
|
+
return () => {
|
|
17
|
+
window.removeEventListener(EVENT, listener);
|
|
18
|
+
window.removeEventListener("storage", listener);
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function setStartupDetails(value: boolean) {
|
|
22
|
+
fallback = value;
|
|
23
|
+
try {
|
|
24
|
+
window.localStorage.setItem(KEY, String(value));
|
|
25
|
+
} catch {
|
|
26
|
+
/* Private storage is optional. */
|
|
27
|
+
}
|
|
28
|
+
window.dispatchEvent(new Event(EVENT));
|
|
29
|
+
}
|
|
30
|
+
/** Presentation preference only. Startup evidence is always retained. */
|
|
31
|
+
export function useStartupDetails() {
|
|
32
|
+
return useSyncExternalStore(subscribe, snapshot, () => false);
|
|
33
|
+
}
|