Files
windmill/frontend/src/lib/components/offboarding-utils.ts
T
f6fcdb5599 feat: open ai chat path links in drawers (#9220)
* feat(ai-chat): link workspace paths and show tool item references

Detect Windmill paths (u/..., f/...) in assistant messages and render
them as clickable pills with the right icon, resolved against a per-
workspace cache. Tool execution headers now list the script/flow/app
paths referenced in tool parameters as external links.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ai-chat): linkify inline-code paths, refine pill styling

- Inline-code spans whose value is exactly a Windmill path now render
  as a link pill (paths inside larger inline code or fenced blocks
  stay as code).
- Tool-header chips moved to their own row to avoid overflow clipping
  when the title wraps.
- Borderless pills, no default background (hover only), kind icons
  use the home-page palette (script blue, flow teal, app orange),
  and the external-link indicator only appears on hover.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ai-chat): linkify variables/resources/triggers + inline drawer

- Workspace item registry now also lists variables, resources, schedules,
  and all 10 trigger kinds; resource wins over variable on path collisions
  (Windmill auto-creates a companion variable for every resource).
- Pill icons delegated to the canonical RowIcon component so each kind
  matches the home-page styling (script blue, flow teal, app orange,
  resource boxes, schedule calendar, etc.).
- Pill href includes the hash fragment each list page already consumes
  (#/resource/<path>, #<path> for variables/schedules/triggers), so
  opening the link puts the user on the list page with the matching
  editor drawer already open.
- For variable and resource pills, a hover-revealed side-panel button
  opens (or toggles closed) the editor drawer inline next to the chat,
  without navigating away. VariableEditor and ResourceEditorDrawer gain
  a closeDrawer() export and forward their close event so the host can
  drive toggling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: simplify ai chat workspace item links

* refactor: keep ai chat path linkification only

* perf: avoid eager ai chat path cache loads

* refactor: simplify ai chat path linking

* feat: open ai chat path links in drawers

* refactor: homogenize workspace item kinds

* fix: toggle ai chat item drawer

* refactor: trim ai chat path cache

* fix: cancel ai chat drawer reopen

---------

Co-authored-by: Guilhem Lemouel <guilhemlemouel@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-05-20 10:00:16 +00:00

116 lines
3.3 KiB
TypeScript

import type { OffboardAffectedPaths } from '$lib/gen'
export function pl(n: number, singular: string): string {
return `${n} ${singular}${n === 1 ? '' : 's'}`
}
function triggerCount(triggers: OffboardAffectedPaths['triggers']): number {
if (!triggers) return 0
return Object.values(triggers).reduce((s, arr) => s + arr.length, 0)
}
export function countPaths(p: OffboardAffectedPaths | undefined | null): number {
if (!p) return 0
return (
(p.scripts?.length ?? 0) +
(p.flows?.length ?? 0) +
(p.apps?.length ?? 0) +
(p.resources?.length ?? 0) +
(p.variables?.length ?? 0) +
(p.schedules?.length ?? 0) +
triggerCount(p.triggers)
)
}
const TRIGGER_TABLE_TO_ROUTE: Record<string, string> = {
http_trigger: 'routes',
websocket_trigger: 'websocket_triggers',
kafka_trigger: 'kafka_triggers',
postgres_trigger: 'postgres_triggers',
mqtt_trigger: 'mqtt_triggers',
nats_trigger: 'nats_triggers',
sqs_trigger: 'sqs_triggers',
gcp_trigger: 'gcp_triggers',
azure_trigger: 'azure_triggers',
email_trigger: 'email_triggers'
}
const TRIGGER_TABLE_TO_LABEL: Record<string, string> = {
http_trigger: 'http trigger',
websocket_trigger: 'websocket trigger',
kafka_trigger: 'kafka trigger',
postgres_trigger: 'postgres trigger',
mqtt_trigger: 'mqtt trigger',
nats_trigger: 'nats trigger',
sqs_trigger: 'sqs trigger',
gcp_trigger: 'gcp trigger',
azure_trigger: 'azure trigger',
email_trigger: 'email trigger'
}
export function flattenPaths(
p: OffboardAffectedPaths | undefined | null
): Array<{ kind: string; path: string }> {
if (!p) return []
const result: Array<{ kind: string; path: string }> = []
for (const [kind, list] of Object.entries(p)) {
if (kind === 'triggers' && list && typeof list === 'object' && !Array.isArray(list)) {
for (const [triggerType, paths] of Object.entries(list as Record<string, string[]>)) {
for (const path of paths) result.push({ kind: triggerType, path })
}
} else if (Array.isArray(list)) {
for (const path of list) result.push({ kind, path })
}
}
return result
}
export function triggerLabel(triggerType: string): string {
return TRIGGER_TABLE_TO_LABEL[triggerType] ?? triggerType
}
export function kindLabel(kind: string): string {
if (TRIGGER_TABLE_TO_LABEL[kind]) return TRIGGER_TABLE_TO_LABEL[kind]
// "scripts" -> "script", "flows" -> "flow", etc.
return kind.replace(/s$/, '')
}
export function itemHref(kind: string, path: string): string | undefined {
switch (kind) {
case 'script':
case 'scripts':
return `/scripts/get/${path}`
case 'flow':
case 'flows':
return `/flows/get/${path}`
case 'app':
case 'apps':
return `/apps/get/${path}`
case 'resource':
case 'resources':
return `/resources#/resource/${path}`
case 'variable':
case 'variables':
return `/variables#${path}`
case 'schedule':
case 'schedules':
return `/schedules#${path}`
default: {
const route = TRIGGER_TABLE_TO_ROUTE[kind]
if (route) return `/${route}#${path}`
return undefined
}
}
}
export function downloadCsv(rows: string[][], filename: string) {
const csv = rows.map((r) => r.map((c) => `"${c.replace(/"/g, '""')}"`).join(',')).join('\n')
const blob = new Blob([csv], { type: 'text/csv' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
a.click()
URL.revokeObjectURL(url)
}