From 2ce618d938f8bb3a3d8cc366b0fdfdb6ce40a5a6 Mon Sep 17 00:00:00 2001 From: Stanislas Polu Date: Thu, 31 Aug 2023 18:33:39 +0200 Subject: [PATCH] enh: sparkle-ization of app design --- front/components/app/Deploy.tsx | 59 +-- front/components/app/NewBlock.tsx | 20 +- front/components/sparkle/navigation.tsx | 5 + front/pages/w/[wId]/a/[aId]/clone.tsx | 385 +++++++++--------- .../w/[wId]/a/[aId]/datasets/[name]/index.tsx | 83 ++-- .../pages/w/[wId]/a/[aId]/datasets/index.tsx | 159 ++++---- front/pages/w/[wId]/a/[aId]/datasets/new.tsx | 59 ++- front/pages/w/[wId]/a/[aId]/execute/index.tsx | 187 +++++---- front/pages/w/[wId]/a/[aId]/index.tsx | 281 +++++++------ .../w/[wId]/a/[aId]/runs/[runId]/index.tsx | 154 +++---- front/pages/w/[wId]/a/[aId]/runs/index.tsx | 250 ++++++------ front/pages/w/[wId]/a/[aId]/settings.tsx | 347 ++++++++-------- front/pages/w/[wId]/a/[aId]/specification.tsx | 26 +- 13 files changed, 1104 insertions(+), 911 deletions(-) diff --git a/front/components/app/Deploy.tsx b/front/components/app/Deploy.tsx index 76b479a42f61..6623b754226d 100644 --- a/front/components/app/Deploy.tsx +++ b/front/components/app/Deploy.tsx @@ -1,13 +1,16 @@ import "@uiw/react-textarea-code-editor/dist.css"; +import { + Button, + ClipboardIcon, + CubeIcon, + DocumentTextIcon, +} from "@dust-tt/sparkle"; import { Dialog, Transition } from "@headlessui/react"; -import { CubeIcon, DocumentDuplicateIcon } from "@heroicons/react/20/solid"; -import { ArrowRightCircleIcon } from "@heroicons/react/24/outline"; import dynamic from "next/dynamic"; import Link from "next/link"; import { Fragment, useState } from "react"; -import { ActionButton, Button, HighlightButton } from "@app/components/Button"; import { useKeys } from "@app/lib/swr"; import { classNames } from "@app/lib/utils"; import { AppType, SpecificationType } from "@app/types/app"; @@ -82,15 +85,15 @@ export default function Deploy({ return (
- { setOpen(!open); }} - > - - Deploy - + disabled={disabled} + icon={CubeIcon} + />
- - - {copyButtonText} - +

@@ -191,23 +196,29 @@ export default function Deploy({ creation parameters, refer to the API reference.

- - - + + +

+
+ +
diff --git a/front/pages/w/[wId]/a/[aId]/datasets/[name]/index.tsx b/front/pages/w/[wId]/a/[aId]/datasets/[name]/index.tsx index 8ae9f840406b..91ef49242380 100644 --- a/front/pages/w/[wId]/a/[aId]/datasets/[name]/index.tsx +++ b/front/pages/w/[wId]/a/[aId]/datasets/[name]/index.tsx @@ -1,12 +1,14 @@ import "@uiw/react-textarea-code-editor/dist.css"; +import { Tab } from "@dust-tt/sparkle"; import { GetServerSideProps, InferGetServerSidePropsType } from "next"; -import Router from "next/router"; +import Router, { useRouter } from "next/router"; import { useEffect, useState } from "react"; import DatasetView from "@app/components/app/DatasetView"; import { ActionButton } from "@app/components/Button"; import AppLayout from "@app/components/sparkle/AppLayout"; +import { AppLayoutSimpleCloseTitle } from "@app/components/sparkle/AppLayoutTitle"; import { subNavigationAdmin, subNavigationApp, @@ -144,6 +146,8 @@ export default function ViewDatasetView({ setIsFinishedEditing(true); }; + const router = useRouter(); + return ( { + void router.push(`/w/${owner.sId}/a`); + }} + /> + } > -
-
-
-
- { - if (!readOnly) { - onUpdate(initializing, valid, currentDatasetInEditor); - } - }} - nameDisabled={true} - /> - - {readOnly ? null : ( -
-
- handleSubmit()} - > - Update - +
+
+ +
+
+
+
+
+ { + if (!readOnly) { + onUpdate(initializing, valid, currentDatasetInEditor); + } + }} + nameDisabled={true} + /> + + {readOnly ? null : ( +
+
+ handleSubmit()} + > + Update + +
-
- )} + )} +
diff --git a/front/pages/w/[wId]/a/[aId]/datasets/index.tsx b/front/pages/w/[wId]/a/[aId]/datasets/index.tsx index acd86801fc62..a962d7fd5b18 100644 --- a/front/pages/w/[wId]/a/[aId]/datasets/index.tsx +++ b/front/pages/w/[wId]/a/[aId]/datasets/index.tsx @@ -1,10 +1,10 @@ -import { PlusIcon, TrashIcon } from "@heroicons/react/20/solid"; +import { Button, PlusIcon, Tab, TrashIcon } from "@dust-tt/sparkle"; import { GetServerSideProps, InferGetServerSidePropsType } from "next"; import Link from "next/link"; -import Router from "next/router"; +import Router, { useRouter } from "next/router"; -import { ActionButton } from "@app/components/Button"; import AppLayout from "@app/components/sparkle/AppLayout"; +import { AppLayoutSimpleCloseTitle } from "@app/components/sparkle/AppLayoutTitle"; import { subNavigationAdmin, subNavigationApp, @@ -88,6 +88,8 @@ export default function DatasetsView({ } }; + const router = useRouter(); + return ( { + void router.push(`/w/${owner.sId}/a`); + }} + /> + } > -
-
-
- - - - New Dataset - - - -
-
    - {datasets.map((d) => { - return ( - -
    +
    + +
    +
    +
    +
    +
    +
    +
    +
      + {datasets.map((d) => { + return ( + -
      -

      - {d.name} -

      - {readOnly ? null : ( -
      - { - e.preventDefault(); - await handleDelete(d.name); - }} - /> -
      - )} -
      -
      -
      -

      - {d.description ? d.description : "No description"} +

      +
      +

      + {d.name}

      + {readOnly ? null : ( +
      + { + e.preventDefault(); + await handleDelete(d.name); + }} + /> +
      + )} +
      +
      +
      +

      + {d.description + ? d.description + : "No description"} +

      +
      -
      - - ); - })} -
    -
    -
    - Datasets are used as input data to apps ( - - input - {" "} - block) or few-shot examples to prompt models ( - - data - {" "} - block). + + ); + })} +
+
+
+ Datasets are used as input data to apps ( + + input + {" "} + block) or few-shot examples to prompt models ( + + data + {" "} + block). +
diff --git a/front/pages/w/[wId]/a/[aId]/datasets/new.tsx b/front/pages/w/[wId]/a/[aId]/datasets/new.tsx index 7e3ae41e9c75..8720e230cc56 100644 --- a/front/pages/w/[wId]/a/[aId]/datasets/new.tsx +++ b/front/pages/w/[wId]/a/[aId]/datasets/new.tsx @@ -1,12 +1,14 @@ import "@uiw/react-textarea-code-editor/dist.css"; +import { Tab } from "@dust-tt/sparkle"; import { GetServerSideProps, InferGetServerSidePropsType } from "next"; -import Router from "next/router"; +import Router, { useRouter } from "next/router"; import { useEffect, useState } from "react"; import DatasetView from "@app/components/app/DatasetView"; import { ActionButton } from "@app/components/Button"; import AppLayout from "@app/components/sparkle/AppLayout"; +import { AppLayoutSimpleCloseTitle } from "@app/components/sparkle/AppLayoutTitle"; import { subNavigationAdmin, subNavigationApp, @@ -117,6 +119,8 @@ export default function NewDatasetView({ setIsFinishedEditing(true); }; + const router = useRouter(); + return ( { + void router.push(`/w/${owner.sId}/a`); + }} + /> + } > -
-
-
- - -
-
- handleSubmit()} - > - Create - +
+
+ +
+
+
+
+ + +
+
+ handleSubmit()} + > + Create + +
diff --git a/front/pages/w/[wId]/a/[aId]/execute/index.tsx b/front/pages/w/[wId]/a/[aId]/execute/index.tsx index d4e6592e180c..c43f678ccc68 100644 --- a/front/pages/w/[wId]/a/[aId]/execute/index.tsx +++ b/front/pages/w/[wId]/a/[aId]/execute/index.tsx @@ -1,3 +1,4 @@ +import { Tab } from "@dust-tt/sparkle"; import { ChevronDownIcon, ChevronRightIcon, @@ -6,6 +7,7 @@ import { } from "@heroicons/react/20/solid"; import { GetServerSideProps, InferGetServerSidePropsType } from "next"; import dynamic from "next/dynamic"; +import { useRouter } from "next/router"; import { useEffect, useState } from "react"; import TextareaAutosize from "react-textarea-autosize"; // TODO: type sse.js or use something else @@ -15,6 +17,7 @@ import { SSE } from "sse.js"; import { Execution } from "@app/components/app/blocks/Output"; import { ActionButton } from "@app/components/Button"; import AppLayout from "@app/components/sparkle/AppLayout"; +import { AppLayoutSimpleCloseTitle } from "@app/components/sparkle/AppLayoutTitle"; import { subNavigationAdmin, subNavigationApp, @@ -558,6 +561,8 @@ export default function ExecuteView({ }; }, [handleKeyPress]); + const router = useRouter(); + return ( { + void router.push(`/w/${owner.sId}/a`); + }} + /> + } > -
-
-
-
-
- This panel lets you use your app on custom{" "} - - input - {" "} - values once finalized.{" "} - {savedRun?.app_hash ? null : ( - <> - You must run your app at least once from the Specification - panel to be able to execute it here with custom values. - - )} +
+
+ +
+
+
+
+
+
+ This panel lets you use your app on custom{" "} + + input + {" "} + values once finalized.{" "} + {savedRun?.app_hash ? null : ( + <> + You must run your app at least once from the Specification + panel to be able to execute it here with custom values. + + )} +
+
+
+
+
+ handleRun()} + > + + Execute + +
-
-
+ {inputDatasetKeys.length ? ( + <> +

Input

+

+ The input fields are inferred from the Dataset attached to + your app's{" "} + + input + {" "} + block. +

+
    + {inputDatasetKeys.map((k) => ( +
  • + handleValueChange(k, value)} + inputType={datasetTypes[k]} + onKeyDown={handleKeyPress} + /> +
  • + ))} +
+ + ) : null} + {executionLogs.blockOrder.length ? (
- handleRun()}> - - Execute - +

+ Execution Trace +

+ { + setOutputExpandedByBlockTypeName((prev) => { + const newExpanded: { [key: string]: boolean } = { + ...prev, + }; + newExpanded[blockName] = !newExpanded[blockName]; + return newExpanded; + }); + }} + />
-
+ ) : null} + {finalOutputBlockTypeName && ( +
+

+ Output +

+ +
+ )}
- {inputDatasetKeys.length ? ( - <> -

Input

-

- The input fields are inferred from the Dataset attached to your - app's{" "} - - input - {" "} - block. -

-
    - {inputDatasetKeys.map((k) => ( -
  • - handleValueChange(k, value)} - inputType={datasetTypes[k]} - onKeyDown={handleKeyPress} - /> -
  • - ))} -
- - ) : null} - {executionLogs.blockOrder.length ? ( -
-

- Execution Trace -

- { - setOutputExpandedByBlockTypeName((prev) => { - const newExpanded: { [key: string]: boolean } = { - ...prev, - }; - newExpanded[blockName] = !newExpanded[blockName]; - return newExpanded; - }); - }} - /> -
- ) : null} - {finalOutputBlockTypeName && ( -
-

Output

- -
- )}
diff --git a/front/pages/w/[wId]/a/[aId]/index.tsx b/front/pages/w/[wId]/a/[aId]/index.tsx index c905a15d7ec0..8dd70b740781 100644 --- a/front/pages/w/[wId]/a/[aId]/index.tsx +++ b/front/pages/w/[wId]/a/[aId]/index.tsx @@ -1,18 +1,21 @@ import { + Button, DocumentDuplicateIcon, - PlayCircleIcon, -} from "@heroicons/react/20/solid"; -import { ArrowRightCircleIcon } from "@heroicons/react/24/outline"; + DocumentTextIcon, + RobotIcon, + SparklesIcon, + Tab, +} from "@dust-tt/sparkle"; import { GetServerSideProps, InferGetServerSidePropsType } from "next"; -import Link from "next/link"; +import { useRouter } from "next/router"; import { useRef, useState } from "react"; import { mutate } from "swr"; import Deploy from "@app/components/app/Deploy"; import NewBlock from "@app/components/app/NewBlock"; import SpecRunView from "@app/components/app/SpecRunView"; -import { ActionButton, Button } from "@app/components/Button"; import AppLayout from "@app/components/sparkle/AppLayout"; +import { AppLayoutSimpleCloseTitle } from "@app/components/sparkle/AppLayoutTitle"; import { subNavigationAdmin, subNavigationApp, @@ -297,6 +300,8 @@ export default function AppView({ }, 0); }; + const router = useRouter(); + return ( { + void router.push(`/w/${owner.sId}/a`); + }} + /> + } > -
-
-
+
+
+ +
+ +
+
{ @@ -322,131 +339,19 @@ export default function AppView({ direction="down" small={false} /> -
-
- handleRun()} - > - - {runRequested || run?.status.run == "running" ? "Running" : "Run"} - -
- {runError ? ( -
- {(() => { - switch (runError.code) { - case "invalid_specification_error": - return `Specification error: ${runError.message}`; - default: - return `Error: ${runError.message}`; - } - })()} -
- ) : null} - {readOnly && user ? ( -
- - - - Clone - - -
- ) : null} -
- {!readOnly ? ( -
- - - -
- ) : null} - {!readOnly && run ? ( -
- -
- ) : null} -
- - - - {spec.length == 0 ? ( -
-

Welcome to your new Dust app.

-

To get started:

-

- - - -

-

...or add your first block!

-
- ) : null} - - {spec.length > 2 && !readOnly ? ( -
-
- { - await handleNewBlock(null, blockType); - }} - spec={spec} - direction="up" - small={false} - /> -
-
- handleRun()} - > - - {runRequested || run?.status.run == "running" - ? "Running" - : "Run"} - -
+ icon={RobotIcon} + /> {runError ? ( -
+
{(() => { switch (runError.code) { case "invalid_specification_error": @@ -457,10 +362,122 @@ export default function AppView({ })()}
) : null} + {readOnly && user ? ( +
+
+ ) : null} +
+ {!readOnly ? ( +
+
+ ) : null} + {!readOnly && run ? ( +
+ +
+ ) : null}
- ) : null} + + + + {spec.length == 0 ? ( +
+

Welcome to your new Dust app.

+

To get started:

+

+

+ ) : null} + + {spec.length > 2 && !readOnly ? ( +
+
+ { + await handleNewBlock(null, blockType); + }} + spec={spec} + direction="up" + small={false} + /> +
+
+
+ {runError ? ( +
+ {(() => { + switch (runError.code) { + case "invalid_specification_error": + return `Specification error: ${runError.message}`; + default: + return `Error: ${runError.message}`; + } + })()} +
+ ) : null} +
+ ) : null} +
+
-
); } diff --git a/front/pages/w/[wId]/a/[aId]/runs/[runId]/index.tsx b/front/pages/w/[wId]/a/[aId]/runs/[runId]/index.tsx index 228b3d76fa01..666104271e8e 100644 --- a/front/pages/w/[wId]/a/[aId]/runs/[runId]/index.tsx +++ b/front/pages/w/[wId]/a/[aId]/runs/[runId]/index.tsx @@ -1,13 +1,16 @@ +import { Tab } from "@dust-tt/sparkle"; import { ArrowLeftCircleIcon, CheckCircleIcon, } from "@heroicons/react/24/outline"; import { GetServerSideProps, InferGetServerSidePropsType } from "next"; +import { useRouter } from "next/router"; import { useState } from "react"; import SpecRunView from "@app/components/app/SpecRunView"; import { ActionButton } from "@app/components/Button"; import AppLayout from "@app/components/sparkle/AppLayout"; +import { AppLayoutSimpleCloseTitle } from "@app/components/sparkle/AppLayoutTitle"; import { subNavigationAdmin, subNavigationApp, @@ -137,6 +140,8 @@ export default function AppRun({ setSavedRunId(run.run_id); }; + const router = useRouter(); + return ( { + void router.push(`/w/${owner.sId}/a`); + }} + /> + } > -
-
-
-
- - Viewing run:{" "} - - {run.run_id} - - - {run.run_id.slice(0, 8)}...{run.run_id.slice(-8)} - - -
- {run.app_hash ? ( -
+
+
+ +
+
+
+
+
- Specification Hash:{" "} - - {run.app_hash} + Viewing run:{" "} + + {run.run_id} - - {run.app_hash.slice(0, 8)}...{run.app_hash.slice(-8)} + + {run.run_id.slice(0, 8)}...{run.run_id.slice(-8)}
- ) : null} + {run.app_hash ? ( +
+ + Specification Hash:{" "} + + {run.app_hash} + + + {run.app_hash.slice(0, 8)}...{run.app_hash.slice(-8)} + + +
+ ) : null} +
+

+ {savedRunId !== run.run_id ? ( + <> + {" "} + + + {isLoading ? "Restoring..." : "Restore"} + + + ) : ( + <> + {" "} + + + Latest version + + + )} +

-

- {savedRunId !== run.run_id ? ( - <> - {" "} - - - {isLoading ? "Restoring..." : "Restore"} - - - ) : ( - <> - {" "} - - - Latest version - - - )} -

+
+ { + // no-op + }} + handleDeleteBlock={() => { + // no-op + }} + handleMoveBlockUp={() => { + // no-op + }} + handleMoveBlockDown={() => { + // no-op + }} + handleNewBlock={() => { + // no-op + }} + /> +
+
-
- { - // no-op - }} - handleDeleteBlock={() => { - // no-op - }} - handleMoveBlockUp={() => { - // no-op - }} - handleMoveBlockDown={() => { - // no-op - }} - handleNewBlock={() => { - // no-op - }} - /> -
-
); } diff --git a/front/pages/w/[wId]/a/[aId]/runs/index.tsx b/front/pages/w/[wId]/a/[aId]/runs/index.tsx index d9af7ad7bab9..5193a835cbb2 100644 --- a/front/pages/w/[wId]/a/[aId]/runs/index.tsx +++ b/front/pages/w/[wId]/a/[aId]/runs/index.tsx @@ -1,9 +1,12 @@ +import { Tab } from "@dust-tt/sparkle"; import { GetServerSideProps, InferGetServerSidePropsType } from "next"; import Link from "next/link"; +import { useRouter } from "next/router"; import { useEffect, useState } from "react"; import { Button } from "@app/components/Button"; import AppLayout from "@app/components/sparkle/AppLayout"; +import { AppLayoutSimpleCloseTitle } from "@app/components/sparkle/AppLayoutTitle"; import { subNavigationAdmin, subNavigationApp, @@ -121,6 +124,8 @@ export default function RunsView({ last = total; } + const router = useRouter(); + return ( { + void router.push(`/w/${owner.sId}/a`); + }} + /> + } > -
- +
+
+ +
+
+ -
-
-
- -
-
- +
+
+
+ +
+
+ +
-
- {runs.length > 0 ? ( -
- Showing runs {offset + 1} - {last} of {total} runs -
- ) : null} + {runs.length > 0 ? ( +
+ Showing runs {offset + 1} - {last} of {total} runs +
+ ) : null} -
-
    - {runs.map((run) => ( -
  • -
    -
    -
    - -

    - {run.run_id.slice(0, 8)}... - {run.run_id.slice(-8)} -

    - -
    -
    -

    - {run.status.run} -

    -
    -
    -
    -
    - {run.status.blocks.map((block) => ( - +
      + {runs.map((run) => ( +
    • +
      +
      +
      + +

      + {run.run_id.slice(0, 8)}... + {run.run_id.slice(-8)} +

      + +
      +
      +

      - {block.name} - - ))} - - ({inputCount(run.status)} inputs) - + {run.status.run} +

      +
      -
      -

      {timeAgoFrom(run.created)} ago

      +
      +
      + {run.status.blocks.map((block) => ( + + {block.name} + + ))} + + ({inputCount(run.status)} inputs) + +
      +
      +

      {timeAgoFrom(run.created)} ago

      +
      +
    • + ))} + {runs.length == 0 ? ( +
      +

      No runs found 🔎

      + {runType == "local" ? ( +

      + Runs triggered from Dust will appear here. +

      + ) : ( +

      + Runs triggered by API will appear here. +

      + )}
      - - ))} - {runs.length == 0 ? ( -
      -

      No runs found 🔎

      - {runType == "local" ? ( -

      - Runs triggered from Dust will appear here. -

      - ) : ( -

      Runs triggered by API will appear here.

      - )} -
      - ) : null} -
    + ) : null} +
+
); diff --git a/front/pages/w/[wId]/a/[aId]/settings.tsx b/front/pages/w/[wId]/a/[aId]/settings.tsx index 5ad9b122224f..8a40a460304c 100644 --- a/front/pages/w/[wId]/a/[aId]/settings.tsx +++ b/front/pages/w/[wId]/a/[aId]/settings.tsx @@ -1,3 +1,4 @@ +import { Tab } from "@dust-tt/sparkle"; import { ChevronRightIcon } from "@heroicons/react/24/outline"; import { GetServerSideProps, InferGetServerSidePropsType } from "next"; import Link from "next/link"; @@ -7,6 +8,7 @@ import { useEffect } from "react"; import { Button } from "@app/components/Button"; import AppLayout from "@app/components/sparkle/AppLayout"; +import { AppLayoutSimpleCloseTitle } from "@app/components/sparkle/AppLayoutTitle"; import { subNavigationAdmin, subNavigationApp, @@ -160,187 +162,204 @@ export default function SettingsView({ subNavigation={subNavigationAdmin({ owner, current: "developers", - subMenuLabel: app.name, - subMenu: subNavigationApp({ owner, app, current: "settings" }), })} + titleChildren={ + { + void router.push(`/w/${owner.sId}/a`); + }} + /> + } > -
-
-
-
-
-
- -
- - {owner.name} - - setAppName(e.target.value)} - /> -
-
- -
-
+
+
+ +
+
+
+
+
+
+
-
- optional +
+ + {owner.name} + + setAppName(e.target.value)} + />
-
- setAppDescription(e.target.value)} - /> -
-

- A good description will help others discover and understand - the purpose of your app. It is also visible at the top of - your app specification. -

-
-
-
- - Visibility - -
-
- { - if (e.target.value != appVisibility) { - setAppVisibility(e.target.value as AppVisibility); - } - }} - /> - -
-
- { - if (e.target.value != appVisibility) { - setAppVisibility(e.target.value as AppVisibility); - } - }} - /> - -
-
- { - if (e.target.value != appVisibility) { - setAppVisibility(e.target.value as AppVisibility); - } - }} - /> - +
+
+ +
+ optional
- {appVisibility == "deleted" ? ( -

- This app is currently marked as deleted. Change its - visibility above to restore it. -

- ) : null} -
+
+ setAppDescription(e.target.value)} + /> +
+

+ A good description will help others discover and + understand the purpose of your app. It is also visible at + the top of your app specification. +

+
+ +
+
+ + Visibility + +
+
+ { + if (e.target.value != appVisibility) { + setAppVisibility( + e.target.value as AppVisibility + ); + } + }} + /> + +
+
+ { + if (e.target.value != appVisibility) { + setAppVisibility( + e.target.value as AppVisibility + ); + } + }} + /> + +
+
+ { + if (e.target.value != appVisibility) { + setAppVisibility( + e.target.value as AppVisibility + ); + } + }} + /> + +
+
+ {appVisibility == "deleted" ? ( +

+ This app is currently marked as deleted. Change its + visibility above to restore it. +

+ ) : null} +
+
-
-
- -
-
- - - -
-
+
+
+
+ + + +
+
+ +
diff --git a/front/pages/w/[wId]/a/[aId]/specification.tsx b/front/pages/w/[wId]/a/[aId]/specification.tsx index 7bee244ca816..b3ccaffa7da7 100644 --- a/front/pages/w/[wId]/a/[aId]/specification.tsx +++ b/front/pages/w/[wId]/a/[aId]/specification.tsx @@ -1,6 +1,9 @@ +import { Tab } from "@dust-tt/sparkle"; import { GetServerSideProps, InferGetServerSidePropsType } from "next"; +import { useRouter } from "next/router"; import AppLayout from "@app/components/sparkle/AppLayout"; +import { AppLayoutSimpleCloseTitle } from "@app/components/sparkle/AppLayoutTitle"; import { subNavigationAdmin, subNavigationApp, @@ -84,6 +87,8 @@ export default function Specification({ specification, gaTrackingId, }: InferGetServerSidePropsType) { + const router = useRouter(); + return ( { + void router.push(`/w/${owner.sId}/a`); + }} + /> + } > -
- {specification} +
+
+ +
+
+ {specification} +
);