From 008e0829dd082907fbbb7897718c7672b8f4ec4c Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra Date: Wed, 26 Aug 2026 12:02:32 +0200 Subject: [PATCH 01/11] /ai: rebuild on Nuxt and share the command block Moves /ai from 11ty to Nuxt, following the strangler-fig migration the repo guide describes, and deletes src/ai.njk so one renderer owns the route. Same page, same copy, same classes: nuxt.config.ts already links /css/style.css and layouts/default.vue already carries .ff-website, so all 223 gated rules apply unchanged. What the port buys: - nuxt/components/content/FfCommand.vue replaces markup plus a per-page copy helper that was hand-pasted into src/platform/device-agent.njk and /ai. Living in components/content means markdown can use it too, so the changelog can carry the same block instead of restating the address. - The agent tabs and the showcase rotation become reactive state rather than roughly 120 lines of querySelectorAll and setInterval in the page. - FAQ structured data goes through useSchemaOrg. The 11ty partial interpolated answers into a JSON string, so markup in an answer landed in the schema. - /ai moves from sitemap-legacy.xml into sitemap.xml. Deliberate changes, not ports: - Sign-up buttons are CtaSignUp, so the copy is the site's fixed "Try it out" and the event is cta-sign-up. The .njk had bespoke labels and reused cta-try-it-out. - Governance and local-agent icons are UIcon lucide equivalents rather than inlined SVG includes. --- nuxt/components/content/FfCommand.vue | 63 +++ nuxt/pages/ai/index.vue | 516 +++++++++++++++++++++ src/ai.njk | 215 --------- src/css/style.css | 97 ++++ src/images/ai/agents/README.md | 25 + src/images/ai/agents/chatgpt.svg | 3 + src/images/ai/agents/claude.svg | 3 + src/images/ai/agents/flowfuse-expert.svg | 2 + src/images/ai/agents/gemini.svg | 10 + src/images/ai/agents/microsoft-copilot.svg | 40 ++ src/images/ai/connect-your-own-agent.svg | 55 +++ src/images/ai/schematic-expert.svg | 20 + src/images/ai/schematic-external.svg | 20 + 13 files changed, 854 insertions(+), 215 deletions(-) create mode 100644 nuxt/components/content/FfCommand.vue create mode 100644 nuxt/pages/ai/index.vue delete mode 100644 src/ai.njk create mode 100644 src/images/ai/agents/README.md create mode 100644 src/images/ai/agents/chatgpt.svg create mode 100644 src/images/ai/agents/claude.svg create mode 100644 src/images/ai/agents/flowfuse-expert.svg create mode 100644 src/images/ai/agents/gemini.svg create mode 100644 src/images/ai/agents/microsoft-copilot.svg create mode 100644 src/images/ai/connect-your-own-agent.svg create mode 100644 src/images/ai/schematic-expert.svg create mode 100644 src/images/ai/schematic-external.svg diff --git a/nuxt/components/content/FfCommand.vue b/nuxt/components/content/FfCommand.vue new file mode 100644 index 0000000000..cbbb0c281c --- /dev/null +++ b/nuxt/components/content/FfCommand.vue @@ -0,0 +1,63 @@ + + + diff --git a/nuxt/pages/ai/index.vue b/nuxt/pages/ai/index.vue new file mode 100644 index 0000000000..ebd1aa875e --- /dev/null +++ b/nuxt/pages/ai/index.vue @@ -0,0 +1,516 @@ + + + diff --git a/src/ai.njk b/src/ai.njk deleted file mode 100644 index 321f9ba389..0000000000 --- a/src/ai.njk +++ /dev/null @@ -1,215 +0,0 @@ ---- -layout: page -nohero: true -imageDescription: "Plant worker interacting with an HMI" -meta: - title: "Industrial AI: FlowFuse Expert, Governed and Built-In" - description: "Industrial AI with FlowFuse: build industrial applications with FlowFuse Expert, act on live operations behind approvals, and give your company-sanctioned AI governed access. Every AI capability on one platform, with the permissions, RBAC, and audit your plants require." -hero: - title: "Industrial AI, Governed and Built-In" - description: - - "AI capability is table stakes in 2026. Governed access to your operations is the difference." - buttonText: "Request a Demo" - buttonLink: "/book-demo/" - buttonReference: "ai" - img: ./images/ai/ai-hero.png - imgAlt: "FlowFuse Expert AI icon with connecting lines flowing out" -capabilityIntro: - title: "Every way AI shows up in your operations" - subtitle: "From accelerating how you build, to acting inside live flows, to connecting the AI your company already trusts. Each one runs on the same governed platform." -capabilityGroups: - - title: "Accelerate engineering" - eyebrow: "Build" - subtitle: "FlowFuse Expert turns intent into working industrial applications, right in the editor." - items: - - name: "AI-assisted engineering" - svgPath: "components/icons/code-bracket.svg" - status: "Available" - statusKind: "live" - description: "Generate and edit Node-RED flows, Function node JavaScript, SQL queries and dashboard UI from plain language, and ask Expert to explain any existing flow so any engineer can pick it up. It works inline in the editor your team already uses, so there is no separate tool to context-switch into. That turns unfamiliar or inherited flows into something the whole team can read and maintain." - - name: "Prompt-to-app" - svgPath: "components/icons/sparkles.svg" - status: "Available" - statusKind: "live" - description: "Describe the application you need and FlowFuse Expert agentically builds the starting flows and logic directly in your workspace. You begin from a working draft instead of a blank canvas, then refine it like any other flow. Everything it creates stays inside the platform, so the same permissions and review apply from the first node." - - name: "Build your own AI agents" - svgPath: "components/icons/squares-plus.svg" - status: "Available" - statusKind: "live" - description: "Start from an agent blueprint, like the LLM chat agent or RAG chat agent, to stand up a task-specific AI agent grounded in your own data, tools and context. It gives you a proven structure to adapt rather than wiring an agent up from scratch. With MCP servers you can give the agent access to anything, including your RAG applications. Because it runs on the platform, the agent operates within the access you grant it." - - name: "Built-in product expert" - svgPath: "components/icons/book-open.svg" - status: "Available" - statusKind: "live" - description: "A chat assistant with answers grounded in FlowFuse and Node-RED documentation, so guidance comes from the product, not stale wikis. Ask how a node works or how to approach a build and get an answer without leaving your workspace. It shortens the path from question to working flow for new and experienced users alike." - - title: "Operate with AI, safely" - eyebrow: "Operate" - subtitle: "AI that acts inside your flows and answers questions about live operations, always behind your controls." - items: - - name: "Governed autonomous operations" - svgPath: "components/icons/bolt.svg" - status: "Available" - statusKind: "live" - description: "Platform Automations let AI act on live systems, with every write behind an approval card, session-scoped and fully audited. A person approves, edits or rejects each proposed change before it reaches a machine. Nothing runs outside the permissions and RBAC that already govern your teams." - - name: "Ask your plant anything" - svgPath: "components/icons/chat-bubble-left-right.svg" - status: "Available" - statusKind: "live" - description: "In Insights mode, ask questions in natural language and get answers grounded in live machine state, alarms and logs. Operators and engineers can check what is happening on the floor without building a report or querying a database by hand. Table and MQTT-broker reading are coming soon." - - name: "Automated visual inspection" - svgPath: "components/icons/camera.svg" - status: "Available" - statusKind: "live" - description: "Run ONNX vision models inside flows next to the machine, with camera ingest over RTSP, for inference that works offline and keeps data on your network. Detection results flow into the same logic as any other signal, so you can trigger alerts or actions from what the model sees. Running at the edge means no round trip to the cloud and no image data leaving the plant." - - name: "Use any model within flows" - svgPath: "components/icons/cog-6-tooth.svg" - status: "Available" - statusKind: "live" - description: "Certified LLM nodes bring OpenAI, Anthropic, Gemini or local models via Ollama into any flow with your own keys. Choose the provider that fits each task, or keep everything on local models when data cannot leave your network. Because you supply the keys, model access and spend stay under your control." - - title: "Connect and govern your AI" - eyebrow: "Connect" - subtitle: "Give the AI your company already trusts a governed door into operations, on a boundary you control." - items: - - name: "Connect your company's AI" - svgPath: "components/icons/arrows-right-left.svg" - status: "Coming soon" - statusKind: "soon" - description: "Expose flows as MCP tools so the AI your company already sanctioned, like Claude or Copilot, can reach operational data through a permissioned boundary. The AI reaches only what you expose, on the terms you set, rather than getting direct access to plant systems. This is our most-requested capability and is in active development." - - name: "AI that uses your tools" - svgPath: "components/icons/puzzle-piece.svg" - status: "Available" - statusKind: "live" - description: "Build your own MCP servers and let Insights-mode agents call your tools and services as part of a workflow. Wrap an internal API or system as a tool once, then let agents use it wherever it fits. The agent stays inside the workflow you designed, calling only the tools you register." -governance: - title: "Governance you can prove" - subtitle: "The control plane every AI action runs through, the same one that governs your teams." - items: - - svgPath: "components/icons/check-circle.svg" - title: "Approval cards on writes" - description: "Every write action AI proposes waits for a human to approve, reject or edit." - - svgPath: "components/icons/lock-closed.svg" - title: "Per-tool permissions" - description: "Grant read, write or delete per tool and per team, so AI only reaches what you allow." - - svgPath: "components/icons/shield-check.svg" - title: "Role-based access" - description: "The same RBAC that governs your teams governs what AI can see and do." - - svgPath: "components/icons/clipboard-document-check.svg" - title: "Audit on everything" - description: "Every action AI touches is logged, so you can show exactly what happened and when." ---- - -
-
-
-
-
-
-

- {{ hero.title | safe }} -

-

The platform is the control plane. AI is the accelerator.

- {% for p in hero.description %} -

{{ p | safe }}

- {% endfor %} -
-
- - {% include "components/ai-chat-interface.njk" %} - - - {% include "components/ai-expert-modal.njk" %} -
-
-
-
-
-
- - -
-
-
-
- {% image "./images/ai/ai-ui.png", "FlowFuse Expert showing AI-generated flow suggestions", [1200] %} -
-
-
-
- -
-
-
-

{{ capabilityIntro.title | safe }}

-

{{ capabilityIntro.subtitle }}

-
- {% for group in capabilityGroups %} -
- -
-

- 0{{ loop.index }} - {{ group.eyebrow }} -

-

{{ group.title }}

-

{{ group.subtitle }}

-
- -
- {% for item in group.items %} -
-
- -
-
- {{ item.name }} - {% if item.statusKind == 'soon' %} - Coming soon - {% endif %} -
-

{{ item.description | safe }}

-
-
-
- {% endfor %} -
-
- {% endfor %} -
-
- -
-
-
-

{{ governance.title | safe }}

-

{{ governance.subtitle }}

-
-
- {% for g in governance.items %} -
-
{% include g.svgPath %}
-

{{ g.title }}

-

{{ g.description }}

-
- {% endfor %} -
-
-
- - - - -
-
-

Where AI meets your operations

-

The use cases pair with FlowFuse AI, from governed agent access to AI-assisted monitoring.

- {% set featuredUseCases = ["production-monitoring", "shop-floor-communication"] %} - {% include "components/use-case-links.njk" %} -

See all use cases

-
-
- diff --git a/src/css/style.css b/src/css/style.css index 0be94f9c0a..0aa1b7d2cb 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -3248,6 +3248,103 @@ lite-youtube::before { height: 2.5rem; color: var(--color-indigo-600); } +/* "Differs by agent" clause: sits under a capability whose behaviour is not identical + on the FlowFuse Expert path and the bring-your-own-agent path. Quieter than the body + copy, and set off by a rule, so it reads as a qualification rather than a feature. */ +.ff-cap-differs { + font-size: 0.875rem; + line-height: 1.6; + color: var(--color-gray-500); + padding-left: 0.875rem; + border-left: 2px solid var(--color-indigo-200); +} + +/* Showcase tabs: text labels with a timing line, not buttons. The line under the active + tab fills over the rotation interval so the switch is expected rather than sudden. */ +.ff-show-tabs { + display: flex; + gap: 1rem; + margin-bottom: 1rem; +} + +/* Each tab takes an equal share of the row, so the timing lines together span the + full width of the well below them. */ +.ff-show-tab { + flex: 1 1 0; + min-width: 0; + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0; + background: none; + border: 0; + text-align: left; + cursor: pointer; +} +.ff-show-tab__label { + font-size: 0.95rem; + font-weight: 400; + color: var(--color-gray-500); + transition: color 0.15s ease; + white-space: nowrap; +} +.ff-show-tab.is-active .ff-show-tab__label, +.ff-show-tab:hover .ff-show-tab__label { + color: var(--color-gray-900); +} +.ff-show-tab.is-active .ff-show-tab__label { font-weight: 500; } + +.ff-show-tab__track { + display: block; + height: 2px; + width: 100%; + background-color: var(--color-gray-200); + overflow: hidden; +} +.ff-show-tab__fill { + display: block; + height: 100%; + width: 0; + background-color: var(--color-indigo-600); +} +.ff-show-tab__fill.is-full { width: 100%; } +.ff-show-tab__fill.is-running { + animation: ffShowFill var(--ff-show-duration, 6000ms) linear forwards; +} +@keyframes ffShowFill { + from { width: 0; } + to { width: 100%; } +} +@media (prefers-reduced-motion: reduce) { + .ff-show-tab__fill.is-running { animation: none; width: 100%; } +} + +/* Agent tab strip: scrolls sideways rather than wrapping, so the row stays one line at + any width. Thin scrollbar so it reads as a control, not a scroll container. */ +.ff-agent-tabs { + scrollbar-width: thin; + -webkit-overflow-scrolling: touch; + overscroll-behavior-x: contain; +} +.ff-agent-tabs::-webkit-scrollbar { + height: 6px; +} +.ff-agent-tabs::-webkit-scrollbar-thumb { + background-color: var(--color-gray-300); + border-radius: 3px; +} + +/* Media well: a reserved 16:9 slot under each capability, for a screenshot or a + short loop. Deliberately visible while empty so the page shows where the assets + go, rather than collapsing and then reflowing when they land. */ +.ff-cap-well { + aspect-ratio: 16 / 9; + width: 100%; + overflow: hidden; + background-color: var(--color-white); + border: 1px solid var(--color-indigo-100); +} + .ff-cap-icon svg { width: 1.875rem; height: 1.875rem; diff --git a/src/images/ai/agents/README.md b/src/images/ai/agents/README.md new file mode 100644 index 0000000000..d4c5dc581f --- /dev/null +++ b/src/images/ai/agents/README.md @@ -0,0 +1,25 @@ +# Agent marks for the /ai connector tabs + +Each tab in the "connect your own agent" section renders `client.logo` when it is set in +`src/ai.njk`, and falls back to the agent's name as text when it is not. + +| File | Tab | Source | +| --- | --- | --- | +| `microsoft-copilot.svg` | Microsoft Copilot | `adoption.microsoft.com/wp-content/uploads/2023/09/icon-copilot.svg`, a Microsoft-owned domain. **Still needs a legal check**: Microsoft's trademark guidelines generally require permission to use its marks in a way that presents a product relationship, and this came from a marketing page rather than a brand kit with usage terms attached. | +| `chatgpt.svg` | ChatGPT | OpenAI brand kit, `OAI_OpenAI-Blossom_Black.svg`, downloaded by hand from `openai.com/brand` because that page blocks automated fetching. This is the OpenAI symbol, which is the icon form their kit provides. | +| `claude.svg` | Claude | Anthropic press kit, `anthropic.com/press-kit`, which redirects to an official CDN zip. File is "Claude Spark - Clay", the icon form intended to sit beside a label. Unmodified, in its own brand colour. | +| `gemini.svg` | Gemini | Google's own CDN, the Gemini sparkle used by Gemini's product surfaces. | +| _(none)_ | Local and Custom Agents | Not a brand mark by design. The repo icon `components/icons/server-stack.svg` is inlined instead of linked as a file, so it inherits `currentColor` and turns white when the tab is active. | + +All marks are used unmodified, at their own aspect ratio, in a 16px-tall slot beside the +agent's name. None is altered in colour or proportion. + +To add or replace one, drop the file here and set the path on that client: + +```yaml + - id: "claude" + logo: "/images/ai/agents/claude.svg" +``` + +Leave `logo` unset until the file exists. Setting it early renders a broken image, whereas +leaving it unset renders the name as text, which is the intended fallback. diff --git a/src/images/ai/agents/chatgpt.svg b/src/images/ai/agents/chatgpt.svg new file mode 100644 index 0000000000..7c119340c4 --- /dev/null +++ b/src/images/ai/agents/chatgpt.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/images/ai/agents/claude.svg b/src/images/ai/agents/claude.svg new file mode 100644 index 0000000000..64fe70ca8f --- /dev/null +++ b/src/images/ai/agents/claude.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/images/ai/agents/flowfuse-expert.svg b/src/images/ai/agents/flowfuse-expert.svg new file mode 100644 index 0000000000..7ce1777f2d --- /dev/null +++ b/src/images/ai/agents/flowfuse-expert.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/images/ai/agents/gemini.svg b/src/images/ai/agents/gemini.svg new file mode 100644 index 0000000000..4545c8331f --- /dev/null +++ b/src/images/ai/agents/gemini.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/images/ai/agents/microsoft-copilot.svg b/src/images/ai/agents/microsoft-copilot.svg new file mode 100644 index 0000000000..f93be73b7a --- /dev/null +++ b/src/images/ai/agents/microsoft-copilot.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/images/ai/connect-your-own-agent.svg b/src/images/ai/connect-your-own-agent.svg new file mode 100644 index 0000000000..118c1de9d1 --- /dev/null +++ b/src/images/ai/connect-your-own-agent.svg @@ -0,0 +1,55 @@ + + How your own agent reaches FlowFuse + + + + + + + + + + + + + + Your own AI agent + Copilot, ChatGPT, Claude + + + signs in + + + + WHAT YOU GRANTED + + + + One FlowFuse address + which teams it may reach, + and whether it may change them + + + + + + FlowFuse + your platform + + + + + + + Instances + and remote + + + Your editor + when you open it + + + Your agent talks to one address. FlowFuse reaches the instances, so they are never opened up. + + diff --git a/src/images/ai/schematic-expert.svg b/src/images/ai/schematic-expert.svg new file mode 100644 index 0000000000..8a9192b5b0 --- /dev/null +++ b/src/images/ai/schematic-expert.svg @@ -0,0 +1,20 @@ + + FlowFuse Expert is part of the platform + + + + + + + + + FlowFuse Platform + Node-RED + + FlowFuse + Expert + + diff --git a/src/images/ai/schematic-external.svg b/src/images/ai/schematic-external.svg new file mode 100644 index 0000000000..b1937b7148 --- /dev/null +++ b/src/images/ai/schematic-external.svg @@ -0,0 +1,20 @@ + + An external agent reaching into the platform + + + + + + + FlowFuse Platform + Node-RED + + + + External AI agent + + From 1b6264b10091e129557e775db101442c2f109930 Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra Date: Wed, 26 Aug 2026 12:36:37 +0200 Subject: [PATCH 02/11] /ai: list the route for prerender Deleting src/ai.njk took /ai out of the 11ty output, and the Nuxt page did not replace it: the prerender crawler only follows links from Nuxt-rendered pages, and every link to /ai lives in 11ty-generated HTML (nav, homepage). So the route never reached nuxt/dist and the link checker reported a bad /ai link from every page that links it. Listed explicitly beside /pricing and /product, which are there for the same reason. --- nuxt/nuxt.config.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nuxt/nuxt.config.ts b/nuxt/nuxt.config.ts index 76ef3c2709..7dadf6157f 100644 --- a/nuxt/nuxt.config.ts +++ b/nuxt/nuxt.config.ts @@ -366,6 +366,10 @@ export default defineNuxtConfig({ '/integrations', '/pricing', '/product', + // /ai is only linked from 11ty-generated HTML (nav, homepage), which the + // Nuxt prerender crawler never parses, so it has to be listed explicitly + // or the route is missing from nuxt/dist and every link to it breaks. + '/ai', ...collectProductRoutes(join(__dirname, 'content/products')), // Without this, @nuxtjs/sitemap only bakes /sitemap.xml statically when // isNuxtGenerate() is true, which checks for nitro.static/preset "static" - From 293539a95e6cc40b213ed90c0f596b49e8c28ab4 Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra Date: Wed, 26 Aug 2026 13:04:24 +0200 Subject: [PATCH 03/11] /ai: inline the repo icons instead of UIcon Four of the six lucide names emitted no CSS rule at all, so those icons rendered as empty spans on the preview: three of the four governance tiles had no glyph, and neither did the Local and Custom Agents tab. Verified on the deploy preview, where only circle-check and chevron-down had a :where(.i-lucide\:...) rule with mask data. The build had warned about it ("failed to load icon lucide:shield-check") and I read that as pre-existing noise, since the same warning fires for icons other pages use. There is no local @iconify-json collection in the repo, so an icon name nothing else already uses has nothing to resolve from. Now inlines the same SVGs src/ai.njk pulled in with {% include %}, which is the more faithful port anyway: no network fetch, and stroke="currentColor" keeps the tab glyph turning white with its label when the tab is active. --- nuxt/pages/ai/index.vue | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/nuxt/pages/ai/index.vue b/nuxt/pages/ai/index.vue index ebd1aa875e..68cac9ede7 100644 --- a/nuxt/pages/ai/index.vue +++ b/nuxt/pages/ai/index.vue @@ -23,6 +23,21 @@ const capture = useCapture() const LINK = 'text-indigo-600 hover:text-indigo-800 underline' +// Icons are the repo's own SVGs, inlined the way src/ai.njk did with +// {% include %}. UIcon was tried first and four of the six lucide names had no +// CSS rule emitted at all (no local @iconify-json collection to resolve them +// from), so those tiles rendered as empty spans. These carry stroke="currentColor" +// so the tab glyph still turns white when its tab is active. +const ICON = { + lock: " ", + shieldCheck: " ", + checkCircle: " ", + clipboardCheck: " ", + serverStack: " ", + chevronDown: "", +} + + const FAQ = [ { question: 'What is AI for industrial applications?', @@ -120,7 +135,7 @@ const CLIENTS = [ }, { id: 'local', - icon: 'i-lucide-server', + icon: ICON.serverStack, name: 'Local and Custom Agents', step2Title: "Your MCP client's config", step2Body: 'Any MCP-capable client works, pointed at your own model, so nothing has to leave your network.', @@ -191,10 +206,10 @@ const GOVERNANCE = { title: 'AI Governance you can prove', subtitle: 'The control plane every AI action runs through, the same one that governs your teams.', items: [ - { icon: 'i-lucide-lock', title: 'Only the teams you grant', description: 'AI reaches the teams you allow and nothing else, enforced by the platform on every call.' }, - { icon: 'i-lucide-shield-check', title: 'Role-based access', description: 'The same RBAC that governs your teams governs what AI can see and do.' }, - { icon: 'i-lucide-circle-check', title: 'Nothing gets deleted', description: 'No agent can remove an instance, an application, a snapshot or a team. There is no tool for it.' }, - { icon: 'i-lucide-clipboard-check', title: 'Audit on everything', description: 'Every action AI takes is logged and attributed, so you can show exactly what happened and when.' }, + { icon: ICON.lock, title: 'Only the teams you grant', description: 'AI reaches the teams you allow and nothing else, enforced by the platform on every call.' }, + { icon: ICON.shieldCheck, title: 'Role-based access', description: 'The same RBAC that governs your teams governs what AI can see and do.' }, + { icon: ICON.checkCircle, title: 'Nothing gets deleted', description: 'No agent can remove an instance, an application, a snapshot or a team. There is no tool for it.' }, + { icon: ICON.clipboardCheck, title: 'Audit on everything', description: 'Every action AI takes is logged and attributed, so you can show exactly what happened and when.' }, ], } @@ -294,7 +309,8 @@ const openFaq = ref(null) :class="activeClient === client.id ? 'ff-btn--primary' : 'text-gray-700 hover:bg-gray-100'" @click="selectClient(client.id)" > -