@unoverse-platform/studio 0.1.0
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/bin/studio.mjs +124 -0
- package/index.html +12 -0
- package/local/catalog.ts +105 -0
- package/local/keys.ts +118 -0
- package/local/nodes.ts +77 -0
- package/local/plugin.ts +401 -0
- package/local/scaffold.d.mts +13 -0
- package/local/scaffold.mjs +85 -0
- package/package.json +43 -0
- package/public/logo.png +0 -0
- package/screens/AtomsView.tsx +206 -0
- package/screens/ComponentsView.tsx +551 -0
- package/screens/ConfigForm.tsx +217 -0
- package/screens/DesignSystemView.tsx +324 -0
- package/screens/IdentityHeader.tsx +88 -0
- package/screens/PromptBlocksView.tsx +137 -0
- package/screens/SkillsView.tsx +167 -0
- package/screens/TemplatesView.tsx +1005 -0
- package/screens/index.ts +26 -0
- package/screens/states.ts +44 -0
- package/src/App.tsx +252 -0
- package/src/ConnectUniverse.tsx +232 -0
- package/src/NewProject.tsx +89 -0
- package/src/NodeKeys.tsx +98 -0
- package/src/NodesView.tsx +443 -0
- package/src/PublishDialog.tsx +309 -0
- package/src/UniverseSession.tsx +121 -0
- package/src/host.ts +165 -0
- package/src/index.css +7 -0
- package/src/main.tsx +11 -0
- package/src/universes.ts +210 -0
- package/vendor/sdk/appEngine.tsx +219 -0
- package/vendor/sdk/lib/UnoverseComponent.tsx +120 -0
- package/vendor/sdk/lib/connection.tsx +302 -0
- package/vendor/sdk/lib/core/actions.ts +84 -0
- package/vendor/sdk/lib/core/client.ts +134 -0
- package/vendor/sdk/lib/core/conditions.ts +43 -0
- package/vendor/sdk/lib/core/connection.ts +142 -0
- package/vendor/sdk/lib/core/index.ts +32 -0
- package/vendor/sdk/lib/core/store.ts +455 -0
- package/vendor/sdk/lib/core/types.ts +194 -0
- package/vendor/sdk/lib/index.ts +59 -0
- package/vendor/sdk/lib/isolate.tsx +70 -0
- package/vendor/sdk/lib/primitives.tsx +453 -0
- package/vendor/sdk/lib/realtime/audioUtils.ts +40 -0
- package/vendor/sdk/lib/realtime/types.ts +45 -0
- package/vendor/sdk/lib/realtime/useAudioCapture.ts +237 -0
- package/vendor/sdk/lib/realtime/useAudioPlayback.ts +145 -0
- package/vendor/sdk/lib/realtime/useRealtimeWebSocket.ts +164 -0
- package/vendor/sdk/lib/render.tsx +178 -0
- package/vendor/sdk/lib/streamed.tsx +65 -0
- package/vendor/sdk/lib/style.ts +227 -0
- package/vendor/sdk/lib/template.tsx +521 -0
- package/vendor/sdk/lib/theme.ts +55 -0
- package/vendor/sdk/lib/voice.tsx +311 -0
- package/vendor/sdk/main.tsx +96 -0
- package/vite.config.ts +70 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt-blocks view — browse the reusable prompt-snippet library
|
|
3
|
+
* (apps/unoverse/prompts/blocks) served by the Unoverse server's /prompt-blocks
|
|
4
|
+
* route. Left: a navigator of blocks grouped by category. Right: the selected block's
|
|
5
|
+
* metadata + full markdown content, with an enable toggle.
|
|
6
|
+
*
|
|
7
|
+
* Ported from apps/unoverse/web/studio/src/views/PromptBlocksView.tsx — kept
|
|
8
|
+
* diffable against the original while both apps exist.
|
|
9
|
+
*
|
|
10
|
+
* A block is referenced in a prompt template as `{{prompt.<camelCaseId>}}`. The toggle
|
|
11
|
+
* (registry kind "prompt", default ON) controls whether the resolver injects it — turning
|
|
12
|
+
* a block OFF drops it from the workflow template context.
|
|
13
|
+
*/
|
|
14
|
+
import { useEffect, useMemo, useState } from "react";
|
|
15
|
+
import { fetchUnoverse } from "studio-host";
|
|
16
|
+
import { usePersistentState, useRegistryToggles, ToggleSwitch } from "studio-host";
|
|
17
|
+
import { IdentityHeader } from "./IdentityHeader";
|
|
18
|
+
|
|
19
|
+
interface PromptBlock {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
content: string;
|
|
24
|
+
tags: string[];
|
|
25
|
+
category?: string;
|
|
26
|
+
enabled?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** "spatial-search" → "spatialSearch" — the key used in `{{prompt.<key>}}`. */
|
|
30
|
+
function toCamelCase(str: string): string {
|
|
31
|
+
return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function PromptBlocksView() {
|
|
35
|
+
const [blocks, setBlocks] = useState<PromptBlock[]>([]);
|
|
36
|
+
const [selected, setSelected] = usePersistentState<string | null>("wb:prompt-blocks:selected", null);
|
|
37
|
+
const [error, setError] = useState<string | null>(null);
|
|
38
|
+
const toggles = useRegistryToggles("prompt");
|
|
39
|
+
|
|
40
|
+
// Load the library once.
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
fetchUnoverse("/prompt-blocks")
|
|
43
|
+
.then((r) => (r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`))))
|
|
44
|
+
.then((d) => {
|
|
45
|
+
const list: PromptBlock[] = d.blocks ?? [];
|
|
46
|
+
setBlocks(list);
|
|
47
|
+
const restore = list.find((b) => b.id === selected) ?? list[0];
|
|
48
|
+
if (restore) setSelected(restore.id);
|
|
49
|
+
})
|
|
50
|
+
.catch((e) => setError(String(e?.message ?? e)));
|
|
51
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
52
|
+
}, []);
|
|
53
|
+
|
|
54
|
+
const detail = useMemo(() => blocks.find((b) => b.id === selected) ?? null, [blocks, selected]);
|
|
55
|
+
|
|
56
|
+
const byCategory = Object.entries(
|
|
57
|
+
blocks.reduce<Record<string, PromptBlock[]>>((acc, b) => {
|
|
58
|
+
(acc[b.category ?? "Uncategorized"] ??= []).push(b);
|
|
59
|
+
return acc;
|
|
60
|
+
}, {}),
|
|
61
|
+
).sort(([a], [b]) => a.localeCompare(b));
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div className="grid min-h-0 flex-1 grid-cols-[220px_1fr] overflow-hidden">
|
|
65
|
+
{/* NAVIGATOR */}
|
|
66
|
+
<nav className="flex min-h-0 min-w-0 flex-col overflow-auto border-r border-gray-700 bg-[#2c2c2e] p-2">
|
|
67
|
+
{blocks.length === 0 && (
|
|
68
|
+
<div className="p-2 text-xs text-gray-400">
|
|
69
|
+
{error
|
|
70
|
+
? `Couldn't load prompt blocks (${error}).`
|
|
71
|
+
: "No prompt blocks. Add .md files to apps/unoverse/prompts/blocks/."}
|
|
72
|
+
</div>
|
|
73
|
+
)}
|
|
74
|
+
{byCategory.map(([cat, items]) => (
|
|
75
|
+
<div key={cat} className="mb-3">
|
|
76
|
+
<div className="px-2 py-1 text-[10px] font-semibold uppercase tracking-wider text-gray-400">{cat}</div>
|
|
77
|
+
{items.map((b) => (
|
|
78
|
+
<button
|
|
79
|
+
key={b.id}
|
|
80
|
+
onClick={() => setSelected(b.id)}
|
|
81
|
+
className={`block w-full rounded px-2 py-1.5 text-left transition ${
|
|
82
|
+
selected === b.id ? "bg-emerald-500/15 text-emerald-300" : "text-gray-300 hover:bg-white/5"
|
|
83
|
+
}`}
|
|
84
|
+
>
|
|
85
|
+
<div className="flex items-center gap-1.5 text-sm font-medium">
|
|
86
|
+
{toggles.isEnabled(b.id) && <span className="h-1.5 w-1.5 rounded-full bg-emerald-400" title="Enabled" />}
|
|
87
|
+
{b.name}
|
|
88
|
+
</div>
|
|
89
|
+
<div className="truncate font-mono text-[11px] text-gray-500">{`{{prompt.${toCamelCase(b.id)}}}`}</div>
|
|
90
|
+
</button>
|
|
91
|
+
))}
|
|
92
|
+
</div>
|
|
93
|
+
))}
|
|
94
|
+
</nav>
|
|
95
|
+
|
|
96
|
+
{/* DETAIL */}
|
|
97
|
+
<div className="min-h-0 min-w-0 overflow-auto bg-white">
|
|
98
|
+
{!detail ? (
|
|
99
|
+
<div className="flex h-full items-center justify-center text-sm text-gray-500">Select a prompt block</div>
|
|
100
|
+
) : (
|
|
101
|
+
<div className="mx-auto max-w-3xl p-8">
|
|
102
|
+
<IdentityHeader
|
|
103
|
+
name={detail.name}
|
|
104
|
+
category={detail.category}
|
|
105
|
+
id={`{{prompt.${toCamelCase(detail.id)}}}`}
|
|
106
|
+
description={detail.description}
|
|
107
|
+
right={<ToggleSwitch on={toggles.isEnabled(detail.id)} onClick={() => toggles.toggle(detail.id)} label="Enabled" />}
|
|
108
|
+
/>
|
|
109
|
+
<p className="mb-3 text-[11px] text-gray-500">
|
|
110
|
+
Reference in a prompt as{" "}
|
|
111
|
+
<span className="font-mono text-gray-600">{`{{prompt.${toCamelCase(detail.id)}}}`}</span>. When off, the resolver
|
|
112
|
+
skips it.
|
|
113
|
+
</p>
|
|
114
|
+
|
|
115
|
+
{detail.tags && detail.tags.length > 0 && (
|
|
116
|
+
<div className="mb-5 flex flex-wrap gap-1.5">
|
|
117
|
+
{detail.tags.map((t) => (
|
|
118
|
+
<span key={t} className="rounded-full bg-gray-900/5 px-2 py-0.5 text-[11px] text-gray-600">
|
|
119
|
+
{t}
|
|
120
|
+
</span>
|
|
121
|
+
))}
|
|
122
|
+
</div>
|
|
123
|
+
)}
|
|
124
|
+
|
|
125
|
+
<div className="mb-2 flex items-center gap-2 text-[10px] font-semibold uppercase tracking-wider text-gray-500">
|
|
126
|
+
<span>Content</span>
|
|
127
|
+
<span className="font-normal normal-case text-gray-600">{detail.id}.md</span>
|
|
128
|
+
</div>
|
|
129
|
+
<pre className="overflow-x-auto whitespace-pre-wrap rounded-lg border border-gray-200 bg-gray-50 p-4 font-mono text-[12.5px] leading-relaxed text-gray-800">
|
|
130
|
+
{detail.content}
|
|
131
|
+
</pre>
|
|
132
|
+
</div>
|
|
133
|
+
)}
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills view — browse the skill library (apps/unoverse/prompts/skills) served by
|
|
3
|
+
* the Unoverse server's /skills routes. Left: a navigator of skills grouped by
|
|
4
|
+
* category. Right: the selected skill's metadata + full SKILL.md instructions.
|
|
5
|
+
*
|
|
6
|
+
* Ported from apps/unoverse/web/studio/src/views/SkillsView.tsx — kept diffable
|
|
7
|
+
* against the original while both apps exist.
|
|
8
|
+
*
|
|
9
|
+
* Read-only: skills are instructions/data for agents (ingested into spatial), never
|
|
10
|
+
* "run". So this is a neutral chrome surface, not a themed render.
|
|
11
|
+
*/
|
|
12
|
+
import { useEffect, useState } from "react";
|
|
13
|
+
import { fetchUnoverse } from "studio-host";
|
|
14
|
+
import { usePersistentState, useRegistryToggles, ToggleSwitch } from "studio-host";
|
|
15
|
+
import { IdentityHeader } from "./IdentityHeader";
|
|
16
|
+
|
|
17
|
+
interface SkillSummary {
|
|
18
|
+
name: string;
|
|
19
|
+
description: string;
|
|
20
|
+
version?: string;
|
|
21
|
+
category?: string;
|
|
22
|
+
filePath: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface SkillDetail extends SkillSummary {
|
|
26
|
+
// Spatial-discovery selection text — ranked against user intent (same contract
|
|
27
|
+
// as nodes/templates).
|
|
28
|
+
whenToUse?: string;
|
|
29
|
+
triggers?: string[];
|
|
30
|
+
instructions: string;
|
|
31
|
+
contentHash: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function SkillsView() {
|
|
35
|
+
const [skills, setSkills] = useState<SkillSummary[]>([]);
|
|
36
|
+
const [selected, setSelected] = usePersistentState<string | null>("wb:skills:selected", null);
|
|
37
|
+
const [detail, setDetail] = useState<SkillDetail | null>(null);
|
|
38
|
+
const [loadingDetail, setLoadingDetail] = useState(false);
|
|
39
|
+
const [error, setError] = useState<string | null>(null);
|
|
40
|
+
const spatial = useRegistryToggles("skill");
|
|
41
|
+
|
|
42
|
+
// Load the library list once.
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
fetchUnoverse("/skills")
|
|
45
|
+
.then((r) => (r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`))))
|
|
46
|
+
.then((d) => {
|
|
47
|
+
const list: SkillSummary[] = d.skills ?? [];
|
|
48
|
+
setSkills(list);
|
|
49
|
+
// Restore the persisted selection across reloads; fall back to the first.
|
|
50
|
+
const restore = list.find((s) => s.name === selected) ?? list[0];
|
|
51
|
+
if (restore) setSelected(restore.name);
|
|
52
|
+
})
|
|
53
|
+
.catch((e) => setError(String(e?.message ?? e)));
|
|
54
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
// Load the full SKILL.md whenever the selection changes.
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (!selected) {
|
|
60
|
+
setDetail(null);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
let cancelled = false;
|
|
64
|
+
setLoadingDetail(true);
|
|
65
|
+
fetchUnoverse(`/skills/${encodeURIComponent(selected)}`)
|
|
66
|
+
.then((r) => (r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`))))
|
|
67
|
+
.then((d) => {
|
|
68
|
+
if (!cancelled) setDetail(d.skill ?? null);
|
|
69
|
+
})
|
|
70
|
+
.catch((e) => {
|
|
71
|
+
if (!cancelled) setError(String(e?.message ?? e));
|
|
72
|
+
})
|
|
73
|
+
.finally(() => {
|
|
74
|
+
if (!cancelled) setLoadingDetail(false);
|
|
75
|
+
});
|
|
76
|
+
return () => {
|
|
77
|
+
cancelled = true;
|
|
78
|
+
};
|
|
79
|
+
}, [selected]);
|
|
80
|
+
|
|
81
|
+
const byCategory = Object.entries(
|
|
82
|
+
skills.reduce<Record<string, SkillSummary[]>>((acc, s) => {
|
|
83
|
+
(acc[s.category ?? "Uncategorized"] ??= []).push(s);
|
|
84
|
+
return acc;
|
|
85
|
+
}, {}),
|
|
86
|
+
).sort(([a], [b]) => a.localeCompare(b));
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<div className="grid min-h-0 flex-1 grid-cols-[220px_1fr] overflow-hidden">
|
|
90
|
+
{/* NAVIGATOR */}
|
|
91
|
+
<nav className="flex min-h-0 min-w-0 flex-col overflow-auto border-r border-gray-700 bg-[#2c2c2e] p-2">
|
|
92
|
+
{skills.length === 0 && (
|
|
93
|
+
<div className="p-2 text-xs text-gray-400">
|
|
94
|
+
{error
|
|
95
|
+
? `Couldn't load skills (${error}).`
|
|
96
|
+
: "No skills in the library. Add SKILL.md files to apps/unoverse/prompts/skills/."}
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
{byCategory.map(([cat, items]) => (
|
|
100
|
+
<div key={cat} className="mb-3">
|
|
101
|
+
<div className="px-2 py-1 text-[10px] font-semibold uppercase tracking-wider text-gray-400">{cat}</div>
|
|
102
|
+
{items.map((s) => (
|
|
103
|
+
<button
|
|
104
|
+
key={s.name}
|
|
105
|
+
onClick={() => setSelected(s.name)}
|
|
106
|
+
className={`block w-full rounded px-2 py-1.5 text-left transition ${
|
|
107
|
+
selected === s.name ? "bg-emerald-500/15 text-emerald-300" : "text-gray-300 hover:bg-white/5"
|
|
108
|
+
}`}
|
|
109
|
+
>
|
|
110
|
+
<div className="flex items-center gap-1.5 text-sm font-medium">
|
|
111
|
+
{spatial.isEnabled(s.name) && <span className="h-1.5 w-1.5 rounded-full bg-emerald-400" title="Spatial on" />}
|
|
112
|
+
{s.name}
|
|
113
|
+
</div>
|
|
114
|
+
<div className="truncate text-[11px] text-gray-500">{s.description}</div>
|
|
115
|
+
</button>
|
|
116
|
+
))}
|
|
117
|
+
</div>
|
|
118
|
+
))}
|
|
119
|
+
</nav>
|
|
120
|
+
|
|
121
|
+
{/* DETAIL */}
|
|
122
|
+
<div className="min-h-0 min-w-0 overflow-auto bg-white">
|
|
123
|
+
{!selected ? (
|
|
124
|
+
<div className="flex h-full items-center justify-center text-sm text-gray-500">Select a skill</div>
|
|
125
|
+
) : (
|
|
126
|
+
<div className="mx-auto max-w-3xl p-8">
|
|
127
|
+
<IdentityHeader
|
|
128
|
+
name={detail?.name ?? selected}
|
|
129
|
+
category={detail?.category}
|
|
130
|
+
version={detail?.version}
|
|
131
|
+
description={detail?.description}
|
|
132
|
+
whenToUse={detail?.whenToUse}
|
|
133
|
+
right={<ToggleSwitch on={spatial.isEnabled(selected)} onClick={() => spatial.toggle(selected)} label="Spatial" />}
|
|
134
|
+
/>
|
|
135
|
+
<p className="mb-3 text-[11px] text-gray-500">When Spatial is on, this skill is embedded as-is into the spatial engine on the next train.</p>
|
|
136
|
+
|
|
137
|
+
{detail?.triggers && detail.triggers.length > 0 && (
|
|
138
|
+
<div className="mb-5 flex flex-wrap gap-1.5">
|
|
139
|
+
{detail.triggers.map((t) => (
|
|
140
|
+
<span key={t} className="rounded-full bg-gray-900/5 px-2 py-0.5 text-[11px] text-gray-600">
|
|
141
|
+
{t}
|
|
142
|
+
</span>
|
|
143
|
+
))}
|
|
144
|
+
</div>
|
|
145
|
+
)}
|
|
146
|
+
|
|
147
|
+
<div className="mb-2 flex items-center gap-2 text-[10px] font-semibold uppercase tracking-wider text-gray-500">
|
|
148
|
+
<span>Instructions</span>
|
|
149
|
+
<span className="font-normal normal-case text-gray-600">SKILL.md</span>
|
|
150
|
+
</div>
|
|
151
|
+
{loadingDetail && !detail ? (
|
|
152
|
+
<div className="text-sm text-gray-500">Loading…</div>
|
|
153
|
+
) : (
|
|
154
|
+
<pre className="overflow-x-auto whitespace-pre-wrap rounded-lg border border-gray-200 bg-gray-50 p-4 font-mono text-[12.5px] leading-relaxed text-gray-800">
|
|
155
|
+
{detail?.instructions ?? ""}
|
|
156
|
+
</pre>
|
|
157
|
+
)}
|
|
158
|
+
|
|
159
|
+
{detail?.contentHash && (
|
|
160
|
+
<div className="mt-4 font-mono text-[10px] text-gray-400">hash: {detail.contentHash.slice(0, 16)}…</div>
|
|
161
|
+
)}
|
|
162
|
+
</div>
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|