Implementing method interfaces in the Web-GUI
This guide explains how to implement a new interactive method interface in DESDEO's Web-GUI. It covers the file structure, the separation between API handling and UI logic, and how to reuse existing components.
This guide is meant as a general guideline, not as an absolute pattern.
Prerequisites
- A working API endpoint for the method (defined in
desdeo/api/routers/). - The endpoint registered in the FastAPI app.
- Orval-generated TypeScript types and endpoint functions
(run
npm run generate:clientafter adding API endpoints).
File structure
Each method lives in its own SvelteKit route directory:
webui/src/routes/interactive_methods/<METHOD>/
+page.ts # Data loader, fetches problems, passes to page
handler.ts # API interaction layer, wraps orval endpoints
+page.svelte # UI component, state, interaction, rendering
+page.server.ts # (optional) Server-side auth guard
types.ts # (optional) Method-specific TypeScript types
helper-functions.ts # (optional) Pure utility functions
Why this structure?
handler.tsisolates all API calls. The page component never imports orval endpoints directly.+page.tsruns before the page renders, loading data the component needs (e.g., the problem list).+page.sveltefocuses on state management and rendering, delegating API work to the handler.types.tsandhelper-functions.tskeep the main files focused.
The handler pattern
Design principle: separate API handling from UI logic.
Each function in handler.ts wraps a single orval-generated endpoint. It calls
the endpoint, checks the response status, and returns typed data or null on
failure. The page component never sees raw HTTP responses. You should never
have the need to manually specify URL's or endpoints; these should be handled
through the Orval generated client. Otherwise, you might run into
problems with user validation, for instance.
Example: E-NAUTILUS handler
// handler.ts
import type { ENautilusStepRequest, ENautilusStepResponse } from "$lib/gen/endpoints/DESDEOFastAPI";
import type { stepMethodEnautilusStepPostResponse } from "$lib/gen/endpoints/DESDEOFastAPI";
import { stepMethodEnautilusStepPost } from "$lib/gen/endpoints/DESDEOFastAPI";
export async function initialize_enautilus_state(
request: ENautilusStepRequest
): Promise<ENautilusStepResponse | null> {
const response: stepMethodEnautilusStepPostResponse =
await stepMethodEnautilusStepPost(request);
if (response.status != 200) {
console.log("E-NAUTILUS init failed.", response.status);
return null;
}
return response.data;
}
Key points:
- Typed inputs and outputs: the function signature documents what it expects and returns.
- Status check: the handler decides what constitutes success, not the UI.
- Returns
nullon failure: the page component checks fornulland logs an error.
Re-exporting shared handlers
Methods often need session management. Rather than reimplementing it, re-export from the shared session handler:
// handler.ts or +page.svelte
import { fetch_sessions, create_session } from '../../methods/sessions/handler';
export { fetch_sessions, create_session };
The page loader (+page.ts)
The loader fetches data before the page renders. Most methods need the problem list:
// +page.ts
import type { PageLoad } from './$types';
import { getProblemsInfoProblemAllInfoGet } from '$lib/gen/endpoints/DESDEOFastAPI';
import type { ProblemInfo } from '$lib/gen/endpoints/DESDEOFastAPI';
type ProblemList = ProblemInfo[];
export const load: PageLoad = async () => {
const res = await getProblemsInfoProblemAllInfoGet();
if (res.status !== 200) throw new Error('Failed to fetch problems');
return { problems: res.data satisfies ProblemList };
};
Reusable components
The Web-GUI provides several composable components that handle common UI patterns across all methods. These should be reused, when possible.
Layout
Every method page should use one of the layout components from $lib/components/custom/method_layout/:
base-layout.svelte: Simple flexbox layout with left sidebar, resizable center panes, and optional right sidebar.explainable-layout.svelte: — Enhanced grid layout with sidebar state management. Use this for methods that need the advanced/explainable sidebar.
Both layouts use Svelte 5 snippets for their slots:
<BaseLayout showLeftSidebar={true} showRightSidebar={false}>
{#snippet leftSidebar()}
<!-- Preference controls go here -->
{/snippet}
{#snippet visualizationArea()}
<!-- Charts and plots go here -->
{/snippet}
{#snippet numericalValues()}
<!-- Solution tables go here -->
{/snippet}
</BaseLayout>
Preferences sidebar
$lib/components/custom/preferences-bar/preferences-sidebar.svelte handles preference input for methods that use reference points, aspiration levels, or classifications.
<PreferencesSidebar
{problem}
preferenceTypes={PREFERENCE_TYPES}
typePreferences={type_preferences}
preferenceValues={current_preference}
objectiveValues={current_objectives}
numSolutions={num_solutions}
onPreferenceChange={handle_preference_change}
/>
Visualizations panel
$lib/components/custom/visualizations-panel/visualizations-panel.svelte renders parallel coordinate plots for comparing solutions.
<VisualizationsPanel
{problem}
previousPreferenceValues={[last_preference]}
currentPreferenceValues={current_preference}
previousPreferenceType={type_preferences}
currentPreferenceType={type_preferences}
solutionsObjectiveValues={objective_values_matrix}
externalSelectedIndexes={selected_indexes}
onSelectSolution={handle_select}
/>
Solution table
$lib/components/custom/nimbus/solution-table.svelte provides a data table with sorting, pagination, save/bookmark, and selection.
<SolutionTable
{problem}
solverResults={solutions}
selectedSolutions={selected_indexes}
handle_row_click={handle_solution_click}
handle_save={handle_save}
isSaved={is_saved}
/>
Dialogs
Reusable dialog utilities from $lib/components/custom/dialogs/dialogs:
import { openConfirmDialog, openInputDialog, openHelpDialog } from '$lib/components/custom/dialogs/dialogs';
// Confirmation before destructive action
const confirmed = await openConfirmDialog("Revert to this iteration?", "This cannot be undone.");
// Prompt for user input
const name = await openInputDialog("Save solution", "Enter a name:");
Putting it together
The data flow through a method interface follows this pattern:
+page.ts (load problems)
|
v
+page.svelte (receive data as props, manage state)
|
|--- user interacts (clicks iterate, selects solution, etc.)
| |
| v
| handler.ts (call API, return typed data)
| |
| v
|--- update state ($state variables)
| |
| v
|--- components react (visualizations, tables, sidebars re-render)
Minimal skeleton
handler.ts:
import { myMethodSolvePost } from "$lib/gen/endpoints/DESDEOFastAPI";
import type { MyMethodRequest, MyMethodResponse } from "$lib/gen/endpoints/DESDEOFastAPI";
export async function solve(request: MyMethodRequest): Promise<MyMethodResponse | null> {
const response = await myMethodSolvePost(request);
if (response.status !== 200) {
console.error("Solve failed:", response.status);
return null;
}
return response.data;
}
+page.ts:
import type { PageLoad } from './$types';
import { getProblemsInfoProblemAllInfoGet } from '$lib/gen/endpoints/DESDEOFastAPI';
export const load: PageLoad = async () => {
const res = await getProblemsInfoProblemAllInfoGet();
if (res.status !== 200) throw new Error('Failed to fetch problems');
return { problems: res.data };
};
+page.svelte:
<script lang="ts">
import { BaseLayout } from '$lib/components/custom/method_layout';
import VisualizationsPanel from '$lib/components/custom/visualizations-panel/visualizations-panel.svelte';
import { solve } from './handler';
import type { ProblemInfo } from '$lib/gen/endpoints/DESDEOFastAPI';
const { data } = $props();
let problem: ProblemInfo | null = $state(null);
let results = $state(null);
async function handleSolve() {
results = await solve({ problem_id: problem.id, /* ... */ });
}
</script>
<BaseLayout>
{#snippet leftSidebar()}
<!-- Preference controls -->
{/snippet}
{#snippet visualizationArea()}
{#if problem && results}
<VisualizationsPanel {problem} solutionsObjectiveValues={results} />
{/if}
{/snippet}
</BaseLayout>