@schlenkr/ragents 0.1.0 → 0.1.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/apps/server/src/plugin-support/language-server/contract.ts +6 -1
- package/apps/server/src/plugin-support/language-server/tools.ts +29 -4
- package/apps/server/src/plugin-support/run-scripts.ts +4 -1
- package/apps/server/src/plugin-support/start-entries-contract.ts +4 -3
- package/apps/server/src/plugin-support/workspace-sandbox-host.ts +2 -0
- package/apps/web/src/App.tsx +0 -10
- package/apps/web/src/PluginChat.tsx +8 -8
- package/apps/web/src/PluginRegistry.tsx +0 -12
- package/apps/web/src/StartSurface.tsx +2 -2
- package/apps/web/src/ThemeSettings.tsx +1 -1
- package/apps/web/src/column/ColumnApp.tsx +83 -7
- package/apps/web/src/column/clipboard.ts +64 -0
- package/apps/web/src/column/host-contract.ts +10 -0
- package/apps/web/src/column.tsx +2 -0
- package/apps/web/src/panel/LoginForm.tsx +9 -3
- package/apps/web/src/panel/OverviewPage.tsx +187 -62
- package/apps/web/src/panel/PanelDialogs.tsx +115 -0
- package/apps/web/src/panel/PanelPage.tsx +1 -1
- package/apps/web/src/panel/SettingsPage.tsx +75 -79
- package/apps/web/src/panel/contract.ts +17 -9
- package/apps/web/src/panel/target-state.ts +27 -2
- package/apps/web/src/ui/theme.css +4 -0
- package/package.json +2 -2
- package/packages/ragents/src/plugin-host.ts +5 -2
- package/packages/ragents/src/plugin-types.ts +3 -2
- package/packages/workspace-executor/src/context.ts +4 -0
- package/packages/workspace-executor/src/executor.ts +9 -2
- package/packages/workspace-executor/src/index.ts +2 -0
- package/packages/workspace-executor/src/language-server/adapters/fsharp.ts +1 -0
- package/packages/workspace-executor/src/language-server/adapters/roslyn.ts +1 -0
- package/packages/workspace-executor/src/language-server/adapters/typescript.ts +34 -6
- package/packages/workspace-executor/src/language-server/contract.ts +7 -3
- package/packages/workspace-executor/src/language-server/host.ts +275 -138
- package/plugins/ragents.actor-programs/contract.ts +1 -1
- package/plugins/ragents.actor-programs/prompt.hbs +4 -4
- package/plugins/ragents.actor-programs/server/app-project.ts +0 -2
- package/plugins/ragents.actor-programs/server/runtime.ts +1 -1
- package/plugins/ragents.actor-programs/web/AppsPanel.tsx +3 -5
- package/plugins/ragents.actor-programs/web/EmbeddedApps.tsx +7 -39
- package/plugins/ragents.actor-programs/web/api.ts +1 -10
- package/plugins/ragents.lsp-roslyn/web/language-server-plugin.tsx +73 -44
- package/plugins/ragents.orchestration/canvas.hbs +28 -44
- package/plugins/ragents.orchestration/contract.ts +17 -287
- package/plugins/ragents.orchestration/server/canvas-tool.ts +18 -123
- package/plugins/ragents.orchestration/tiled-layout.ts +12 -4
- package/plugins/ragents.orchestration/web/ActorCanvasControls.tsx +23 -47
- package/plugins/ragents.orchestration/web/ActorChatPreview.tsx +3 -9
- package/plugins/ragents.orchestration/web/ActorShortcut.tsx +3 -5
- package/plugins/ragents.orchestration/web/ActorTile.tsx +3 -2
- package/plugins/ragents.orchestration/web/CanvasTiles.tsx +3 -3
- package/plugins/ragents.orchestration/web/Center.tsx +28 -463
- package/plugins/ragents.orchestration/web/MaterialBody.tsx +6 -87
- package/plugins/ragents.orchestration/web/TiledCanvas.tsx +2 -3
- package/plugins/ragents.orchestration/web/canvas-view-settings.ts +7 -13
- package/plugins/ragents.orchestration/web/column/RunColumn.tsx +44 -19
- package/plugins/ragents.orchestration/web/column/column-state.ts +13 -4
- package/plugins/ragents.orchestration/web/constants.ts +0 -2
- package/plugins/ragents.orchestration/web/index.tsx +0 -6
- package/plugins/ragents.orchestration/web/tile-docking.ts +7 -1
- package/plugins/ragents.orchestration/web/tiled-view-settings.ts +7 -9
- package/plugins/ragents.overseer/server/coordinator-prompt.ts +1 -1
- package/plugins/ragents.reference/examples.ts +1 -2
- package/plugins/ragents.reference/run-scripts/balcony-wizard/RUN.md +1 -1
- package/plugins/ragents.reference/run-scripts/balcony-wizard/actors/balcony-app/package.json +1 -1
- package/plugins/ragents.reference/run-scripts/balcony-wizard/src/server.ts +1 -1
- package/plugins/ragents.reference/run-scripts/balcony-wizard/tests/program.test.ts +2 -2
- package/plugins/ragents.reference/run-scripts/conversation-circle/RUN.md +3 -4
- package/plugins/ragents.reference/run-scripts/conversation-circle/src/server.ts +9 -15
- package/plugins/ragents.reference/run-scripts/conversation-circle/tests/program.test.ts +5 -3
- package/plugins/ragents.reference/run-scripts/learning-afternoon/package.json +1 -1
- package/plugins/ragents.reference/run-scripts/learning-afternoon/src/server.ts +14 -4
- package/plugins/ragents.reference/run-scripts/word-game/src/server.ts +1 -1
- package/plugins/ragents.reference/skills/210-moderator-handover/SKILL.md +1 -1
- package/plugins/ragents.reference/walkthroughs.ts +14 -14
- package/plugins/ragents.workspace/client/workspace-client.ts +4 -1
- package/scripts/package/publish-package.ts +4 -5
- package/scripts/workspace-client/run-workspace-client.ts +3 -1
- package/apps/web/src/MaterialSettings.tsx +0 -40
- package/apps/web/src/material-settings.ts +0 -104
- package/plugins/ragents.orchestration/web/ActorNet.tsx +0 -607
- package/plugins/ragents.orchestration/web/CardSizeSettings.tsx +0 -42
- package/plugins/ragents.orchestration/web/ScriptActorCard.tsx +0 -56
- package/plugins/ragents.orchestration/web/ZoomSettings.tsx +0 -38
- package/plugins/ragents.orchestration/web/canvas-layout-storage.ts +0 -39
- package/plugins/ragents.orchestration/web/canvas-layout.ts +0 -326
- package/plugins/ragents.orchestration/web/canvas-scene.ts +0 -311
- package/plugins/ragents.orchestration/web/card-size-settings.ts +0 -36
- package/plugins/ragents.orchestration/web/keyboard-navigation.ts +0 -55
- package/plugins/ragents.orchestration/web/material-layout.ts +0 -14
- package/plugins/ragents.orchestration/web/useCanvasMeasurements.ts +0 -78
- package/plugins/ragents.orchestration/web/zoom-settings.ts +0 -28
- package/plugins/ragents.reference/skills/120-canvas-conversation-circle/SKILL.md +0 -12
- package/plugins/ragents.reference/skills/130-canvas-pipeline-with-approval/SKILL.md +0 -12
- package/plugins/ragents.reference/skills/140-canvas-team-tree-and-topic-grid/SKILL.md +0 -12
|
@@ -38,6 +38,13 @@ const contract = {
|
|
|
38
38
|
const startMarker = "START_LEARNING_AFTERNOON";
|
|
39
39
|
|
|
40
40
|
|
|
41
|
+
type Tile = { entity: string } | { direction: "vertical"; weights: [number, number]; children: [Tile, Tile] };
|
|
42
|
+
|
|
43
|
+
function stackedTiles(entities: string[]): Tile {
|
|
44
|
+
const [first, ...rest] = entities;
|
|
45
|
+
return rest.length === 0 ? { entity: first! } : { direction: "vertical", weights: [1, rest.length], children: [{ entity: first! }, stackedTiles(rest)] };
|
|
46
|
+
}
|
|
47
|
+
|
|
41
48
|
function boardWith(helpers: HelperState[]): LearningState {
|
|
42
49
|
const working = helpers.some((helper) => helper.status === "working");
|
|
43
50
|
return { phase: working ? "working" : helpers.some((helper) => helper.status === "error") ? "error" : "complete", helpers };
|
|
@@ -65,10 +72,13 @@ export default defineActor(contract, {
|
|
|
65
72
|
const actor = await context.functions.agent_spawn({ handle: `learning-${helper.id}`, displayName: helper.label, profile: profile.name, prompt, tools: [] });
|
|
66
73
|
return { ...helper, actorId: actor.id };
|
|
67
74
|
}));
|
|
68
|
-
await context.functions.canvas_layout_replace({
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
75
|
+
await context.functions.canvas_layout_replace({
|
|
76
|
+
root: {
|
|
77
|
+
direction: "horizontal",
|
|
78
|
+
weights: [1, 1],
|
|
79
|
+
children: [{ entity: `app:@${context.actor.handle}/main` }, stackedTiles(helpers.map((helper) => `@learning-${helper.id}`))],
|
|
80
|
+
},
|
|
81
|
+
});
|
|
72
82
|
await context.functions.run_configure({ title: learningWorkflow.title, primaryActor: `@${context.actor.handle}` });
|
|
73
83
|
context.state.replace({ board: { phase: "ready", helpers } });
|
|
74
84
|
return;
|
|
@@ -37,7 +37,7 @@ export default defineActor(contract, {
|
|
|
37
37
|
try {
|
|
38
38
|
if (!state.status) {
|
|
39
39
|
await context.functions.run_configure({ title: "Wortspiel", primaryActor: context.actor.id });
|
|
40
|
-
await context.functions.canvas_layout_replace({
|
|
40
|
+
await context.functions.canvas_layout_replace({ root: { entity: `app:@${context.actor.handle}/main` } });
|
|
41
41
|
const start = payloadOf(JSON.parse(input.content));
|
|
42
42
|
if (start.input !== null || !start.options || typeof start.options !== "object" || Array.isArray(start.options)
|
|
43
43
|
|| Object.keys(start).some((key) => key !== "input" && key !== "options")) {
|
|
@@ -6,7 +6,7 @@ title: "Gespräch an einen Moderator übergeben"
|
|
|
6
6
|
description: "Zeigt die Übergabe eines laufenden Chats an einen anderen Primary-Actor. Der Moderator übernimmt als direkter Ansprechpartner und hält den Aufgabenfortschritt fest."
|
|
7
7
|
category: "Zusammenarbeit"
|
|
8
8
|
order: 210
|
|
9
|
-
tags: "Rückfragen, To-dos, Anwendungsfall, Konzeptdemo, Agententeams, Primary-Actor
|
|
9
|
+
tags: "Rückfragen, To-dos, Anwendungsfall, Konzeptdemo, Agententeams, Primary-Actor"
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
Ich hätte gern einen Workshop für einen ruhigeren Arbeitstag: Ein Planer bringt zwei Ideen mit, ein skeptischer Gast prüft die Nachteile. Ein Moderator übernimmt die Runde und spricht danach direkt mit mir im Chat, damit ich einen Vorschlag auswählen kann. Zeig die drei auf der Fläche und übergib das Gespräch wirklich an den Moderator. Seine Aufgabenliste zeigt den Fortschritt.
|
|
@@ -2,25 +2,25 @@ export const referenceWalkthroughs = [
|
|
|
2
2
|
{
|
|
3
3
|
id: "mini-app-owner-inspector",
|
|
4
4
|
title: "Den Helfer hinter einer Mini-App öffnen",
|
|
5
|
-
description: "Die Mini-App bleibt
|
|
6
|
-
tags: ["Anwendungsfall", "Konzeptdemo", "
|
|
5
|
+
description: "Die Mini-App bleibt als Kachel sichtbar; ihr Besitzer ist auch ohne eigene Kachel erreichbar.",
|
|
6
|
+
tags: ["Anwendungsfall", "Konzeptdemo", "Kachelfläche"],
|
|
7
7
|
steps: [
|
|
8
|
-
"Im Profil core Sammelboard einrichten öffnen und den Leitfaden abschließen. Erwartung: Die Liste steht auf
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"Die
|
|
8
|
+
"Im Profil core Sammelboard einrichten öffnen und den Leitfaden abschließen. Erwartung: Die Liste steht als Kachel auf der Fläche; ihr Besitzer hat keine eigene Kachel.",
|
|
9
|
+
"In der Kopfzeile Actors öffnen und den Namen des Listenbesitzers wählen. Erwartung: Rechts öffnen sich dessen Chat und Details, ohne die Kachelaufteilung zu ändern.",
|
|
10
|
+
"Den Besitzer aus der Kopfzeile auf die Kachel der Liste ziehen und rechts andocken. Erwartung: Seine Kachel erscheint neben der Liste; die Mini-App bleibt bedienbar und behält ihre Einträge.",
|
|
11
|
+
"Die neue Kachel über das Kreuz in ihrer Kopfzeile wieder entfernen und die App erneut bedienen. Erwartung: Actor und Funktionen arbeiten weiter; sein Name bleibt in der Kopfzeile erreichbar.",
|
|
12
12
|
],
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
|
-
id: "personal-
|
|
16
|
-
title: "Eine Gesprächsrunde
|
|
17
|
-
description: "
|
|
18
|
-
tags: ["Anwendungsfall", "Konzeptdemo", "
|
|
15
|
+
id: "personal-tile-arrangement",
|
|
16
|
+
title: "Eine Gesprächsrunde anders aufteilen",
|
|
17
|
+
description: "Kacheln lassen sich persönlich umstellen und wieder auf die Programmvorgabe zurücksetzen.",
|
|
18
|
+
tags: ["Anwendungsfall", "Konzeptdemo", "Kachelfläche"],
|
|
19
19
|
steps: [
|
|
20
|
-
"Im Profil core den
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
20
|
+
"Im Profil core den Einstieg Gesprächsrunde einrichten ausführen und den Aufbau abwarten. Die vom Run gesetzte Aufteilung ansehen.",
|
|
21
|
+
"Eine Kachel an ihrer Kopfzeile fassen und am oberen Rand einer anderen Kachel andocken. Erwartung: Die Aufteilung ändert sich sofort; alle Gesprächspartner arbeiten weiter.",
|
|
22
|
+
"Die Trennlinie zwischen zwei Kacheln verschieben und denselben Run im selben Browser neu laden. Erwartung: Die persönliche Aufteilung bleibt erhalten; die Actors und die Programmanordnung sind unverändert.",
|
|
23
|
+
"In der Statusleiste Programmvorgabe übernehmen wählen. Erwartung: Die Fläche zeigt wieder die Aufteilung des Runs; der Knopf verschwindet, bis wieder eine eigene Änderung vorliegt.",
|
|
24
24
|
],
|
|
25
25
|
},
|
|
26
26
|
{
|
|
@@ -37,6 +37,8 @@ export interface WorkspaceClientExecution {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export interface WorkspaceClientOptions {
|
|
40
|
+
/** Die Host-Wurzel dieses Rechners, aus der die Sprachserver aufgelöst werden; sie kann erst später entstehen. */
|
|
41
|
+
hostRoot: () => string | undefined;
|
|
40
42
|
onExecuted?: (execution: WorkspaceClientExecution) => void;
|
|
41
43
|
}
|
|
42
44
|
|
|
@@ -75,7 +77,7 @@ export class WorkspaceClient {
|
|
|
75
77
|
constructor(
|
|
76
78
|
private readonly transport: WorkspaceClientTransport,
|
|
77
79
|
identity: WorkspaceClientIdentity,
|
|
78
|
-
private readonly options: WorkspaceClientOptions
|
|
80
|
+
private readonly options: WorkspaceClientOptions,
|
|
79
81
|
) {
|
|
80
82
|
this.#identity = { ...identity, folders: [...identity.folders] };
|
|
81
83
|
this.#executor = new SandboxWorkspaceExecutor({
|
|
@@ -198,6 +200,7 @@ export class WorkspaceClient {
|
|
|
198
200
|
root: await realPathOf(cwd),
|
|
199
201
|
home: { home: homedir() },
|
|
200
202
|
logDirectory: join(tmpdir(), "ragents-workspace-logs", input.runId),
|
|
203
|
+
hostRoot: this.options.hostRoot(),
|
|
201
204
|
additions: input.env,
|
|
202
205
|
}));
|
|
203
206
|
const startedAt = Date.now();
|
|
@@ -80,6 +80,7 @@ export const publishPlan = (manifest: Record<string, unknown>, published: readon
|
|
|
80
80
|
|
|
81
81
|
export interface PublishOptions {
|
|
82
82
|
readonly directory: string;
|
|
83
|
+
readonly version: string;
|
|
83
84
|
readonly token: string;
|
|
84
85
|
readonly dryRun: boolean;
|
|
85
86
|
readonly npm: NpmRunner;
|
|
@@ -92,7 +93,7 @@ const scopeAdvice = (output: string): string => /Scope not found/i.test(output)
|
|
|
92
93
|
+ "auf npmjs.com anlegen bzw. den Token für den Scope freigeben."
|
|
93
94
|
: "";
|
|
94
95
|
|
|
95
|
-
/** Veröffentlicht den gebauten Ordner
|
|
96
|
+
/** Veröffentlicht den gebauten Ordner; der Exit 0 von npm publish ist die Bestätigung. */
|
|
96
97
|
export const publishDirectory = (options: PublishOptions): void => {
|
|
97
98
|
const hide = redacting(options.token);
|
|
98
99
|
const published = options.npm(["publish", "--access", "public", ...options.dryRun ? ["--dry-run"] : []], { cwd: options.directory, token: options.token });
|
|
@@ -103,9 +104,7 @@ export const publishDirectory = (options: PublishOptions): void => {
|
|
|
103
104
|
options.log("== Probelauf: nichts veröffentlicht");
|
|
104
105
|
return;
|
|
105
106
|
}
|
|
106
|
-
|
|
107
|
-
if (view.status !== 0) throw new Error(`npm view ${PACKAGE_NAME} version endete mit Code ${view.status}: ${hide(oneLine(view.stderr))}`);
|
|
108
|
-
options.log(`== Auf npm steht jetzt ${PACKAGE_NAME}@${view.stdout.trim()}`);
|
|
107
|
+
options.log(`== Veröffentlicht: ${PACKAGE_NAME}@${options.version}`);
|
|
109
108
|
};
|
|
110
109
|
|
|
111
110
|
const main = async (): Promise<void> => {
|
|
@@ -117,7 +116,7 @@ const main = async (): Promise<void> => {
|
|
|
117
116
|
const built = await buildPackage(path.join(repositoryRoot, "dist", PACKAGE_FOLDER));
|
|
118
117
|
const plan = publishPlan(built.manifest, publishedVersions(PACKAGE_NAME, runNpm));
|
|
119
118
|
console.log(`== ${plan.name}@${plan.version} aus Host ${plan.hostVersion.slice(0, 12)}, ${built.fileCount} Dateien, ${(built.byteSize / 1024 / 1024).toFixed(1)} MB`);
|
|
120
|
-
publishDirectory({ directory: built.directory, token, dryRun, npm: runNpm, log: (line) => console.log(line) });
|
|
119
|
+
publishDirectory({ directory: built.directory, version: plan.version, token, dryRun, npm: runNpm, log: (line) => console.log(line) });
|
|
121
120
|
};
|
|
122
121
|
|
|
123
122
|
const moduleUrl: string | undefined = import.meta.url;
|
|
@@ -3,6 +3,7 @@ import { statSync } from "node:fs";
|
|
|
3
3
|
import { hostname } from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { hostRoot } from "../../apps/server/src/host-version.ts";
|
|
6
7
|
import { provisionWorkspace } from "../../apps/server/src/profile/provisioning.ts";
|
|
7
8
|
import { workspaceClientTransport } from "../../plugins/ragents.workspace/client/transport.ts";
|
|
8
9
|
import { WorkspaceClient } from "../../plugins/ragents.workspace/client/workspace-client.ts";
|
|
@@ -13,7 +14,7 @@ dieses Rechners aus - dasselbe, was die VS-Code-Erweiterung tut, nur ohne VS Cod
|
|
|
13
14
|
gilt das aktuelle Verzeichnis. Jeder Werkzeugaufruf erscheint als eine Zeile auf stdout.
|
|
14
15
|
Die Sprachserver holt der Start in den Werkzeugordner dieses Rechners (dasselbe wie
|
|
15
16
|
pnpm provision --workspace); ROSLYN_LANGUAGE_SERVER und FSHARP_LANGUAGE_SERVER übersteuern ihn,
|
|
16
|
-
TypeScript
|
|
17
|
+
TypeScript kommt aus dem Host-Ordner. Das Skript läuft, bis es mit Strg-C beendet wird.`;
|
|
17
18
|
|
|
18
19
|
export interface WorkspaceClientArguments {
|
|
19
20
|
readonly serverUrl: string;
|
|
@@ -71,6 +72,7 @@ const main = async (): Promise<void> => {
|
|
|
71
72
|
};
|
|
72
73
|
const transport = workspaceClientTransport(parsed.serverUrl, process.env.RAGENTS_TOKEN);
|
|
73
74
|
const client = new WorkspaceClient(transport, identity, {
|
|
75
|
+
hostRoot,
|
|
74
76
|
onExecuted: ({ runId, tool, durationMs, error }) =>
|
|
75
77
|
console.log(`== ${runId.slice(0, 8)} ${tool} ${durationMs} ms ${error ?? "ok"}`),
|
|
76
78
|
});
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { useEffect, useId, useState } from "react";
|
|
2
|
-
import { useAccess } from "./AccessContext";
|
|
3
|
-
import { DEFAULT_MATERIAL_SETTINGS, MATERIAL_SETTINGS_LIMITS, resetMaterialSettings, saveMaterialSettings, useMaterialSettings } from "./material-settings";
|
|
4
|
-
import { Button, Input } from "./ui";
|
|
5
|
-
|
|
6
|
-
const noteClasses = "text-sm leading-[1.6] text-muted-foreground";
|
|
7
|
-
|
|
8
|
-
export function MaterialSettings() {
|
|
9
|
-
const { settings, error } = useMaterialSettings();
|
|
10
|
-
const writable = useAccess().can("settings.write");
|
|
11
|
-
const prefix = useId();
|
|
12
|
-
const [steps, setSteps] = useState(String(settings.steps));
|
|
13
|
-
useEffect(() => { setSteps(String(settings.steps)); }, [settings]);
|
|
14
|
-
const valid = (text: string, min: number, max: number) => text.trim() !== "" && Number.isInteger(Number(text)) && Number(text) >= min && Number(text) <= max;
|
|
15
|
-
const stepsValid = valid(steps, MATERIAL_SETTINGS_LIMITS.minSteps, MATERIAL_SETTINGS_LIMITS.maxSteps);
|
|
16
|
-
const changeSteps = (value: string) => {
|
|
17
|
-
if (!writable) return;
|
|
18
|
-
setSteps(value);
|
|
19
|
-
if (valid(value, MATERIAL_SETTINGS_LIMITS.minSteps, MATERIAL_SETTINGS_LIMITS.maxSteps)) saveMaterialSettings({ ...settings, steps: Number(value) });
|
|
20
|
-
};
|
|
21
|
-
return <div className="flex min-w-0 max-w-[680px] flex-col gap-5">
|
|
22
|
-
<p className={noteClasses}>Schichtwerk mit matten Flächen und gestapelter Tiefe. Änderungen gelten sofort für alle Runs und werden in diesem Browser gespeichert.</p>
|
|
23
|
-
<div className="flex flex-col gap-2">
|
|
24
|
-
<label className="text-sm font-semibold text-foreground" id={`${prefix}-steps-label`} htmlFor={`${prefix}-steps`}>Tiefenstufen</label>
|
|
25
|
-
<div className="flex min-w-0 items-center gap-5">
|
|
26
|
-
<input className="w-0 min-w-0 flex-1 accent-primary" aria-labelledby={`${prefix}-steps-label`} disabled={!writable} type="range" min={MATERIAL_SETTINGS_LIMITS.minSteps} max={MATERIAL_SETTINGS_LIMITS.maxSteps} step="1" value={settings.steps} onChange={(event) => changeSteps(event.target.value)} />
|
|
27
|
-
<Input className="w-[84px] flex-none" id={`${prefix}-steps`} aria-invalid={!stepsValid} aria-describedby={`${prefix}-steps-note`} disabled={!writable} type="number" min={MATERIAL_SETTINGS_LIMITS.minSteps} max={MATERIAL_SETTINGS_LIMITS.maxSteps} step="1" value={steps} onChange={(event) => changeSteps(event.target.value)} />
|
|
28
|
-
</div>
|
|
29
|
-
<p id={`${prefix}-steps-note`} className={stepsValid ? noteClasses : "text-sm leading-[1.6] text-destructive"}>{stepsValid ? `0 ist flach, bis zu 5 gleich tiefe Stufen. Standard: ${DEFAULT_MATERIAL_SETTINGS.steps}.` : "Bitte eine ganze Zahl von 0 bis 5 eingeben."}</p>
|
|
30
|
-
</div>
|
|
31
|
-
<div><Button variant="outline" disabled={!writable} onClick={() => {
|
|
32
|
-
if (!writable) return;
|
|
33
|
-
if (resetMaterialSettings()) {
|
|
34
|
-
setSteps(String(DEFAULT_MATERIAL_SETTINGS.steps));
|
|
35
|
-
}
|
|
36
|
-
}}>Auf Standard zurücksetzen</Button></div>
|
|
37
|
-
{!writable && <p className={noteClasses}>Die Materialeinstellungen sind mit deinen Rechten nur lesbar.</p>}
|
|
38
|
-
{error && <p className="text-sm leading-[1.6] text-destructive" role="alert">{error}</p>}
|
|
39
|
-
</div>;
|
|
40
|
-
}
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import { useSyncExternalStore } from "react";
|
|
2
|
-
import { subscribeStorageChanges } from "./lib/local-storage-setting";
|
|
3
|
-
|
|
4
|
-
export interface MaterialSettings {
|
|
5
|
-
steps: number;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export const DEFAULT_MATERIAL_SETTINGS: MaterialSettings = Object.freeze({ steps: 1 });
|
|
9
|
-
export const MATERIAL_SETTINGS_LIMITS = { minSteps: 0, maxSteps: 5 };
|
|
10
|
-
export const MATERIAL_DEPTH_PER_STEP = 12;
|
|
11
|
-
export const MATERIAL_SETTINGS_STORAGE_KEY = "ragents.material-settings";
|
|
12
|
-
|
|
13
|
-
interface MaterialSettingsSnapshot {
|
|
14
|
-
settings: MaterialSettings;
|
|
15
|
-
error?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function parseMaterialSettings(raw: string | null): MaterialSettings {
|
|
19
|
-
if (raw === null) return DEFAULT_MATERIAL_SETTINGS;
|
|
20
|
-
const value: unknown = JSON.parse(raw);
|
|
21
|
-
if (!value || typeof value !== "object" || !("steps" in value)
|
|
22
|
-
|| typeof value.steps !== "number" || !Number.isInteger(value.steps) || value.steps < MATERIAL_SETTINGS_LIMITS.minSteps || value.steps > MATERIAL_SETTINGS_LIMITS.maxSteps) {
|
|
23
|
-
throw new Error("Ungültiges Material: Tiefenstufen müssen eine ganze Zahl von 0 bis 5 sein. Bitte die Materialeinstellungen zurücksetzen.");
|
|
24
|
-
}
|
|
25
|
-
return Object.freeze({ steps: value.steps });
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const errorMessage = (cause: unknown) => cause instanceof Error ? cause.message : String(cause);
|
|
29
|
-
const serverSnapshot: MaterialSettingsSnapshot = { settings: DEFAULT_MATERIAL_SETTINGS };
|
|
30
|
-
const getServerSnapshot = () => serverSnapshot;
|
|
31
|
-
const subscribeServer = () => () => {};
|
|
32
|
-
|
|
33
|
-
export function createMaterialSettingsStore(browser: Window) {
|
|
34
|
-
let snapshot: MaterialSettingsSnapshot;
|
|
35
|
-
try {
|
|
36
|
-
snapshot = { settings: parseMaterialSettings(browser.localStorage.getItem(MATERIAL_SETTINGS_STORAGE_KEY)) };
|
|
37
|
-
} catch (cause) {
|
|
38
|
-
snapshot = { settings: DEFAULT_MATERIAL_SETTINGS, error: `Das gespeicherte Material konnte nicht geladen werden. Die Standardwerte werden angezeigt. ${errorMessage(cause)}` };
|
|
39
|
-
}
|
|
40
|
-
const listeners = new Set<() => void>();
|
|
41
|
-
let disposed = false;
|
|
42
|
-
const publish = (next: MaterialSettingsSnapshot) => {
|
|
43
|
-
if (snapshot.settings.steps === next.settings.steps && snapshot.error === next.error) return;
|
|
44
|
-
snapshot = next;
|
|
45
|
-
for (const listener of listeners) listener();
|
|
46
|
-
};
|
|
47
|
-
const report = (message: string) => publish({ ...snapshot, error: message });
|
|
48
|
-
const storageChanged = (event: StorageEvent) => {
|
|
49
|
-
try {
|
|
50
|
-
const storage = browser.localStorage;
|
|
51
|
-
if (event.storageArea !== storage) return;
|
|
52
|
-
publish({ settings: parseMaterialSettings(storage.getItem(MATERIAL_SETTINGS_STORAGE_KEY)) });
|
|
53
|
-
} catch (cause) {
|
|
54
|
-
report(`Das Material aus einem anderen Browser-Tab konnte nicht übernommen werden. ${errorMessage(cause)}`);
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
const unsubscribeStorage = subscribeStorageChanges(browser, (key) => key === MATERIAL_SETTINGS_STORAGE_KEY, storageChanged);
|
|
58
|
-
return {
|
|
59
|
-
getSnapshot: () => snapshot,
|
|
60
|
-
subscribe(listener: () => void) {
|
|
61
|
-
if (disposed) throw new Error("Die Materialeinstellungen wurden bereits beendet.");
|
|
62
|
-
listeners.add(listener);
|
|
63
|
-
return () => { listeners.delete(listener); };
|
|
64
|
-
},
|
|
65
|
-
save(settings: MaterialSettings) {
|
|
66
|
-
if (disposed) throw new Error("Die Materialeinstellungen wurden bereits beendet.");
|
|
67
|
-
try {
|
|
68
|
-
const next = parseMaterialSettings(JSON.stringify(settings));
|
|
69
|
-
browser.localStorage.setItem(MATERIAL_SETTINGS_STORAGE_KEY, JSON.stringify(next));
|
|
70
|
-
publish({ settings: next });
|
|
71
|
-
} catch (cause) {
|
|
72
|
-
report(`Das Material konnte nicht gespeichert werden. ${errorMessage(cause)}`);
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
reset() {
|
|
76
|
-
if (disposed) throw new Error("Die Materialeinstellungen wurden bereits beendet.");
|
|
77
|
-
try {
|
|
78
|
-
browser.localStorage.removeItem(MATERIAL_SETTINGS_STORAGE_KEY);
|
|
79
|
-
publish({ settings: DEFAULT_MATERIAL_SETTINGS });
|
|
80
|
-
return true;
|
|
81
|
-
} catch (cause) {
|
|
82
|
-
report(`Das Material konnte nicht zurückgesetzt werden. ${errorMessage(cause)}`);
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
},
|
|
86
|
-
dispose() {
|
|
87
|
-
if (disposed) return;
|
|
88
|
-
disposed = true;
|
|
89
|
-
unsubscribeStorage();
|
|
90
|
-
listeners.clear();
|
|
91
|
-
},
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
let activeStore: ReturnType<typeof createMaterialSettingsStore> | undefined;
|
|
96
|
-
const materialSettingsStore = () => activeStore ??= createMaterialSettingsStore(window);
|
|
97
|
-
|
|
98
|
-
export function useMaterialSettings(): MaterialSettingsSnapshot {
|
|
99
|
-
const store = typeof window === "undefined" ? undefined : materialSettingsStore();
|
|
100
|
-
return useSyncExternalStore(store?.subscribe ?? subscribeServer, store?.getSnapshot ?? getServerSnapshot, getServerSnapshot);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export const saveMaterialSettings = (settings: MaterialSettings): void => materialSettingsStore().save(settings);
|
|
104
|
-
export const resetMaterialSettings = (): boolean => materialSettingsStore().reset();
|