From 3a3caf54966cf399ccc36e7ddacfdc3fb6c742e0 Mon Sep 17 00:00:00 2001 From: dev Date: Thu, 1 Oct 2026 11:31:26 +0800 Subject: [PATCH] web: give a tool its own page instead of sharing the index --- docs/design.md | 16 +++ frontend/src/lib/i18n.ts | 1 + frontend/src/pages/Tools.svelte | 231 +++++++++++++++----------------- 3 files changed, 127 insertions(+), 121 deletions(-) diff --git a/docs/design.md b/docs/design.md index 1b10697..fe305f4 100644 --- a/docs/design.md +++ b/docs/design.md @@ -399,6 +399,22 @@ reused. Neither is as fast as naming the content. The service registers no service worker and emits no manifest, so there is no second cache layer to reason about. +## Tool routes + +The tool route has two states. Without a tool identifier the page is the +index: one card per tool, grouped by category, each card opening the tool. +With an identifier the page is that tool on its own. + +The two states are one route rather than two pages. A single route means the +back button, a shared link, and a reload all behave the same way whether the +reader arrived from the index or from a link elsewhere. The tool page carries +a link back to the index, because the index is not on it. + +The tool page holds no list. That is the point of the split: a reader who +opens a link to a tool sees the form rather than the list it was reached +from, and the form gets the whole width. The cost is that comparing two +tools takes two round trips through the index. + ## Testing Unit tests live beside the code they test and cover the parser, the codec, diff --git a/frontend/src/lib/i18n.ts b/frontend/src/lib/i18n.ts index c638ad8..e5b92cf 100644 --- a/frontend/src/lib/i18n.ts +++ b/frontend/src/lib/i18n.ts @@ -298,6 +298,7 @@ const STRINGS: Record = { 'api.itemCount': { zh: '个接口项', en: 'items' }, 'tools.open': { zh: '打开', en: 'Open' }, + 'tools.back': { zh: '全部工具', en: 'All tools' }, 'tools.choose': { zh: '共 {count} 个工具,按类别分组。', en: '{count} tools, grouped by category.' }, }; diff --git a/frontend/src/pages/Tools.svelte b/frontend/src/pages/Tools.svelte index 41a0b95..371f4c0 100644 --- a/frontend/src/pages/Tools.svelte +++ b/frontend/src/pages/Tools.svelte @@ -2,6 +2,12 @@ /** * The online tools. * + * The route has two states. Without a tool identifier the page is the + * index: one card per tool, grouped by category, each card opening the + * tool. With an identifier the page is that tool on its own, so a link + * that names a tool shows the form rather than the list it was reached + * from. The index carries a way back. + * * The form is generated from the tool description the backend supplies, * so a tool that is added to the registry appears here without a change to * this component. Running a tool posts the form values as JSON and renders @@ -24,9 +30,9 @@ let lang = $derived($language); /** - * The page root, measured so that every tool card shares one height. The - * cards are spread over one grid per category, so the measurement has to - * start from the element that contains all of them. + * The index, measured so that every tool card shares one height. The cards + * are spread over one grid per category, so the measurement has to start + * from the element that contains all of them. */ let page = $state(null); $effect(() => uniformCards(page)); @@ -61,32 +67,17 @@ const currentRegistry = registry; const currentId = id; if (!currentRegistry) return; - // With no tool named in the route the first one is shown, so the page - // opens on a working form rather than an invitation to pick. - const wanted = - (currentId - ? currentRegistry.tools.find((candidate) => candidate.id === currentId) - : undefined) ?? currentRegistry.tools[0]; - const found = wanted ?? null; + // A tool is opened by naming it in the route. Without a name the page is + // the index, and no tool is selected. + const found = currentId + ? (currentRegistry.tools.find((candidate) => candidate.id === currentId) ?? null) + : null; tool = found; result = null; error = null; values = defaultsFor(found); }); - /** - * Moves the form into view after a tool is chosen. - * - * The index and the form share the page, so a reader who picks a tool from - * the list would otherwise stay on the index and not see the form. This - * runs only when the route names a tool, which is what a click does. - */ - let form = $state(null); - $effect(() => { - if (!id || !form) return; - form.scrollIntoView({ behavior: 'smooth', block: 'start' }); - }); - /** Runs the selected tool. */ async function run() { if (!tool) return; @@ -143,14 +134,105 @@
- - {#if !registry}

{tr('tools.loading', lang)}

+{:else if tool} + + + +
+
+

{pick(tool.name, lang)}

+ {pick(tool.category, lang)} +
+

{pick(tool.description, lang)}

+ +
{ + event.preventDefault(); + void run(); + }} + > +
+ {#each tool.params as param} + {@const invalid = error?.parameter === param.name} +
+ + {#if param.kind === 'select'} + + {:else if param.kind === 'multiline'} + + {:else if param.kind === 'number' || param.kind === 'integer'} + + {:else} + + {/if} + {pick(param.help, lang)} + {#if param.constraints.unit} + {tr('tools.unit', lang)}: {param.constraints.unit} + {/if} +
+ {/each} +
+ +
+ + +
+
+ + {#if tool.api_items.length > 0} +
+ {tr('tools.calls', lang)} + {#each tool.api_items as name} + {name} + {/each} +
+ {/if} +
+ + {#if error} +
+

+ {#if error.parameter}{error.parameter}:{/if} + {error.message} +

+
+ {/if} + + {#if result} +
+ +
+ {/if} {:else} + + + {#each categories as group}

{pick(group.category, lang)}

@@ -172,98 +254,5 @@
{/each} - - {#if !tool} -
-

{tr('tools.select', lang)}

-
- {:else} -
-
-

{pick(tool.name, lang)}

- {pick(tool.category, lang)} -
-

{pick(tool.description, lang)}

- -
{ - event.preventDefault(); - void run(); - }} - > -
- {#each tool.params as param} - {@const invalid = error?.parameter === param.name} -
- - {#if param.kind === 'select'} - - {:else if param.kind === 'multiline'} - - {:else if param.kind === 'number' || param.kind === 'integer'} - - {:else} - - {/if} - {pick(param.help, lang)} - {#if param.constraints.unit} - {tr('tools.unit', lang)}: {param.constraints.unit} - {/if} -
- {/each} -
- -
- - -
-
- - {#if tool.api_items.length > 0} -
- {tr('tools.calls', lang)} - {#each tool.api_items as name} - {name} - {/each} -
- {/if} -
- - {#if error} -
-

- {#if error.parameter} - {error.parameter}: - {/if} - {error.message} -

-
- {/if} - - {#if result} -
- -
- {/if} - {/if} {/if}