AI dashboard widget + CLI AI generate fix suggestions (#1925)
* Made a better AI icon and improved how it looks on the create new schedules inspector * WIP adding kapa ai to the app * WIP adding a new Ask AI button to the side menu * WIP using the react example from the docs * Align the AI button in the bottom bar * Kapa widget now works * Trigger the Kapa modal from the custom button * Fix imports * Adds Ask AI shortcut to Shortcuts panel * Adds a new enter shortcut key * Adds a prop so you can optionally hide the shortcut key * Latest * Moved the Kapa/Help stuff into a component, out of root * WIP using onModalClose * Creates a wrapper provider to block shortcuts while kapa modal is open (has bugs) * Fixes button alignment * Hide the shortcut key at the button layer * Fix for enable/disable shortcut keys globally * Kapa is working * You can bring up the shortcut keys without opening the help panel * TODO remove listeners * remove imports and fix invalid tailwind class * style kapa widget as best i can * Remove Kapa event listeners * Allow passing in a query * Open the AI widget if there’s a URL param * Much cleaner implementation for Kapa * Trying to auto-open the Kapa widget when the page loads * Delay opening the widget because it was causing issues * Improved Kapa widget colors * Added an AI help link to the CLI * We don’t need this anymore * Exit with 1, indicating an error. This is important for CI * Removed old auto-opening code * Added some code comments to explain some of the annoying stuff --------- Co-authored-by: James Ritchie <james@trigger.dev>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Added AI assistance link when you have build errors
|
||||
@@ -1,53 +1,31 @@
|
||||
export function AISparkleIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg
|
||||
className={className}
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M14.9806 0.803884C14.8871 0.33646 14.4767 0 14 0C13.5233 0 13.1129 0.33646 13.0194 0.803884L12.7809 1.99644C12.7017 2.3923 12.3923 2.70174 11.9964 2.78091L10.8039 3.01942C10.3365 3.1129 10 3.52332 10 4C10 4.47668 10.3365 4.8871 10.8039 4.98058L11.9964 5.21909C12.3923 5.29826 12.7017 5.6077 12.7809 6.00356L13.0194 7.19612C13.1129 7.66354 13.5233 8 14 8C14.4767 8 14.8871 7.66354 14.9806 7.19612L15.2191 6.00356C15.2983 5.6077 15.6077 5.29826 16.0036 5.21909L17.1961 4.98058C17.6635 4.8871 18 4.47668 18 4C18 3.52332 17.6635 3.1129 17.1961 3.01942L16.0036 2.78091C15.6077 2.70174 15.2983 2.3923 15.2191 1.99644L14.9806 0.803884Z"
|
||||
fill="url(#paint0_linear_11402_36656)"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M9.14286 4.85718C9.46177 4.85718 9.74205 5.06859 9.82966 5.37523L10.6041 8.08589C10.9431 9.27235 11.8705 10.1998 13.057 10.5388L15.7677 11.3132C16.0743 11.4008 16.2857 11.6811 16.2857 12C16.2857 12.319 16.0743 12.5992 15.7677 12.6868L13.057 13.4613C11.8705 13.8003 10.9431 14.7277 10.6041 15.9142L9.82966 18.6248C9.74205 18.9315 9.46177 19.1429 9.14286 19.1429C8.82394 19.1429 8.54367 18.9315 8.45605 18.6248L7.68158 15.9142C7.34259 14.7277 6.41517 13.8003 5.22871 13.4613L2.51806 12.6868C2.21141 12.5992 2 12.319 2 12C2 11.6811 2.21141 11.4008 2.51806 11.3132L5.22871 10.5388C6.41517 10.1998 7.34259 9.27235 7.68158 8.08589L8.45605 5.37523C8.54367 5.06859 8.82394 4.85718 9.14286 4.85718Z"
|
||||
fill="#7655FD"
|
||||
/>
|
||||
<path
|
||||
d="M5.94868 4.68377C5.81257 4.27543 5.43043 4 5 4C4.56957 4 4.18743 4.27543 4.05132 4.68377L3.36754 6.73509C3.26801 7.03369 3.03369 7.26801 2.73509 7.36754L0.683772 8.05132C0.27543 8.18743 0 8.56957 0 9C0 9.43043 0.27543 9.81257 0.683772 9.94868L2.73509 10.6325C3.03369 10.732 3.26801 10.9663 3.36754 11.2649L4.05132 13.3162C4.18743 13.7246 4.56957 14 5 14C5.43043 14 5.81257 13.7246 5.94868 13.3162L6.63246 11.2649C6.73199 10.9663 6.96631 10.732 7.26491 10.6325L9.31623 9.94868C9.72457 9.81257 10 9.43043 10 9C10 8.56957 9.72457 8.18743 9.31623 8.05132L7.26491 7.36754C6.96631 7.26801 6.73199 7.03369 6.63246 6.73509L5.94868 4.68377Z"
|
||||
fill="url(#paint1_linear_11402_36656)"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M17.7143 2C18.0421 2 18.3278 2.22307 18.4072 2.54105L18.6538 3.5272C18.8777 4.42291 19.5771 5.12229 20.4728 5.34622L21.459 5.59276C21.7769 5.67225 22 5.95795 22 6.28571C22 6.61348 21.7769 6.89918 21.459 6.97867L20.4728 7.22521C19.5771 7.44914 18.8777 8.14851 18.6538 9.04423L18.4072 10.0304C18.3278 10.3484 18.0421 10.5714 17.7143 10.5714C17.3865 10.5714 17.1008 10.3484 17.0213 10.0304L16.7748 9.04423C16.5509 8.14852 15.8515 7.44914 14.9558 7.22521L13.9696 6.97867C13.6516 6.89918 13.4286 6.61348 13.4286 6.28571C13.4286 5.95795 13.6516 5.67225 13.9696 5.59276L14.9558 5.34622C15.8515 5.12229 16.5509 4.42291 16.7748 3.5272L17.0213 2.54105C17.1008 2.22307 17.3865 2 17.7143 2Z"
|
||||
fill="#D946EF"
|
||||
/>
|
||||
<path
|
||||
d="M12.9487 12.6838C12.8126 12.2754 12.4304 12 12 12C11.5696 12 11.1874 12.2754 11.0513 12.6838L10.8675 13.2351C10.768 13.5337 10.5337 13.768 10.2351 13.8675L9.68377 14.0513C9.27543 14.1874 9 14.5696 9 15C9 15.4304 9.27543 15.8126 9.68377 15.9487L10.2351 16.1325C10.5337 16.232 10.768 16.4663 10.8675 16.7649L11.0513 17.3162C11.1874 17.7246 11.5696 18 12 18C12.4304 18 12.8126 17.7246 12.9487 17.3162L13.1325 16.7649C13.232 16.4663 13.4663 16.232 13.7649 16.1325L14.3162 15.9487C14.7246 15.8126 15 15.4304 15 15C15 14.5696 14.7246 14.1874 14.3162 14.0513L13.7649 13.8675C13.4663 13.768 13.232 13.5337 13.1325 13.2351L12.9487 12.6838Z"
|
||||
fill="url(#paint2_linear_11402_36656)"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M16.2857 14.8572C16.5932 14.8572 16.8661 15.0539 16.9633 15.3456L17.3388 16.472C17.481 16.8986 17.8157 17.2333 18.2423 17.3755L19.3687 17.751C19.6604 17.8482 19.8571 18.1212 19.8571 18.4286C19.8571 18.7361 19.6604 19.009 19.3687 19.1062L18.2423 19.4817C17.8157 19.6239 17.481 19.9586 17.3388 20.3852L16.9633 21.5116C16.8661 21.8033 16.5932 22 16.2857 22C15.9783 22 15.7053 21.8033 15.6081 21.5116L15.2326 20.3852C15.0904 19.9586 14.7557 19.6239 14.3291 19.4817L13.2027 19.1062C12.911 19.009 12.7143 18.7361 12.7143 18.4286C12.7143 18.1212 12.911 17.8482 13.2027 17.751L14.3291 17.3755C14.7557 17.2333 15.0904 16.8986 15.2326 16.472L15.6081 15.3456C15.7053 15.0539 15.9783 14.8572 16.2857 14.8572Z"
|
||||
fill="#4F46E5"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_11402_36656"
|
||||
x1="9"
|
||||
y1="0"
|
||||
x2="9"
|
||||
y2="18"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#E543FF" />
|
||||
<stop offset="1" stopColor="#286399" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_11402_36656"
|
||||
x1="9"
|
||||
y1="0"
|
||||
x2="9"
|
||||
y2="18"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#E543FF" />
|
||||
<stop offset="1" stopColor="#286399" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint2_linear_11402_36656"
|
||||
x1="9"
|
||||
y1="0"
|
||||
x2="9"
|
||||
y2="18"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#E543FF" />
|
||||
<stop offset="1" stopColor="#286399" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
export function KeyboardEnterIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12 5H18C19.6569 5 21 6.34315 21 8V13C21 14.6569 19.6569 16 18 16H8"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<path d="M2 16L8 12L8 20L2 16Z" fill="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
} from "./primitives/SheetV3";
|
||||
import { ShortcutKey } from "./primitives/ShortcutKey";
|
||||
import { Button } from "./primitives/Buttons";
|
||||
import { useState } from "react";
|
||||
import { useShortcutKeys } from "~/hooks/useShortcutKeys";
|
||||
|
||||
export function Shortcuts() {
|
||||
return (
|
||||
@@ -23,121 +25,147 @@ export function Shortcuts() {
|
||||
data-action="shortcuts"
|
||||
fullWidth
|
||||
textAlignLeft
|
||||
shortcut={{ modifiers: ["shift"], key: "?" }}
|
||||
shortcut={{ modifiers: ["shift"], key: "?", enabled: false }}
|
||||
className="gap-x-0 pl-0.5"
|
||||
iconSpacing="gap-x-0.5"
|
||||
>
|
||||
Shortcuts
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Keyboard className="size-5 text-indigo-500" />
|
||||
<span className="font-sans text-base font-medium text-text-bright">
|
||||
Keyboard shortcuts
|
||||
</span>
|
||||
</div>
|
||||
</SheetTitle>
|
||||
<div className="space-y-6 px-4 pb-4 pt-2">
|
||||
<div className="space-y-3">
|
||||
<Header3>General</Header3>
|
||||
<Shortcut name="Close">
|
||||
<ShortcutKey shortcut={{ key: "esc" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Confirm">
|
||||
<ShortcutKey shortcut={{ modifiers: ["mod"] }} variant="medium/bright" />
|
||||
<ShortcutKey shortcut={{ key: "enter" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Filter">
|
||||
<ShortcutKey shortcut={{ key: "f" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Select filter">
|
||||
<ShortcutKey shortcut={{ key: "1" }} variant="medium/bright" />
|
||||
<Paragraph variant="small" className="ml-1.5">
|
||||
to
|
||||
</Paragraph>
|
||||
<ShortcutKey shortcut={{ key: "9" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Previous page">
|
||||
<ShortcutKey shortcut={{ key: "j" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Next page">
|
||||
<ShortcutKey shortcut={{ key: "k" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Help & Feedback">
|
||||
<ShortcutKey shortcut={{ key: "h" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<Header3>Runs page</Header3>
|
||||
<Shortcut name="Bulk action: Cancel runs">
|
||||
<ShortcutKey shortcut={{ key: "c" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Bulk action: Replay runs">
|
||||
<ShortcutKey shortcut={{ key: "r" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Bulk action: Clear selection">
|
||||
<ShortcutKey shortcut={{ key: "esc" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<Header3>Run page</Header3>
|
||||
<Shortcut name="Replay run">
|
||||
<ShortcutKey shortcut={{ key: "r" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Overview">
|
||||
<ShortcutKey shortcut={{ key: "o" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Details">
|
||||
<ShortcutKey shortcut={{ key: "d" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Context">
|
||||
<ShortcutKey shortcut={{ key: "c" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Metadata">
|
||||
<ShortcutKey shortcut={{ key: "m" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Navigate">
|
||||
<ShortcutKey shortcut={{ key: "arrowup" }} variant="medium/bright" />
|
||||
<ShortcutKey shortcut={{ key: "arrowdown" }} variant="medium/bright" />
|
||||
<ShortcutKey shortcut={{ key: "arrowleft" }} variant="medium/bright" />
|
||||
<ShortcutKey shortcut={{ key: "arrowright" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Expand all">
|
||||
<ShortcutKey shortcut={{ key: "e" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Collapse all">
|
||||
<ShortcutKey shortcut={{ key: "w" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Toggle level">
|
||||
<ShortcutKey shortcut={{ key: "0" }} variant="medium/bright" />
|
||||
<Paragraph variant="small" className="ml-1.5">
|
||||
to
|
||||
</Paragraph>
|
||||
<ShortcutKey shortcut={{ key: "9" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<Header3>Schedules page</Header3>
|
||||
<Shortcut name="New schedule">
|
||||
<ShortcutKey shortcut={{ key: "n" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<Header3>Alerts page</Header3>
|
||||
<Shortcut name="New alert">
|
||||
<ShortcutKey shortcut={{ key: "n" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
</div>
|
||||
</div>
|
||||
</SheetHeader>
|
||||
</SheetContent>
|
||||
<ShortcutContent />
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
export function ShortcutsAutoOpen() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
useShortcutKeys({
|
||||
shortcut: { modifiers: ["shift"], key: "?" },
|
||||
action: () => {
|
||||
setIsOpen(true);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Sheet open={isOpen} onOpenChange={setIsOpen}>
|
||||
<ShortcutContent />
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
function ShortcutContent() {
|
||||
return (
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Keyboard className="size-5 text-indigo-500" />
|
||||
<span className="font-sans text-base font-medium text-text-bright">
|
||||
Keyboard shortcuts
|
||||
</span>
|
||||
</div>
|
||||
</SheetTitle>
|
||||
<div className="space-y-6 px-4 pb-4 pt-2">
|
||||
<div className="space-y-3">
|
||||
<Header3>General</Header3>
|
||||
<Shortcut name="Close">
|
||||
<ShortcutKey shortcut={{ key: "esc" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Confirm">
|
||||
<ShortcutKey shortcut={{ modifiers: ["mod"] }} variant="medium/bright" />
|
||||
<ShortcutKey shortcut={{ key: "enter" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Ask AI">
|
||||
<ShortcutKey shortcut={{ modifiers: ["mod"], key: "/" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Filter">
|
||||
<ShortcutKey shortcut={{ key: "f" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Select filter">
|
||||
<ShortcutKey shortcut={{ key: "1" }} variant="medium/bright" />
|
||||
<Paragraph variant="small" className="ml-1.5">
|
||||
to
|
||||
</Paragraph>
|
||||
<ShortcutKey shortcut={{ key: "9" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Previous page">
|
||||
<ShortcutKey shortcut={{ key: "j" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Next page">
|
||||
<ShortcutKey shortcut={{ key: "k" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Help & Feedback">
|
||||
<ShortcutKey shortcut={{ key: "h" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<Header3>Runs page</Header3>
|
||||
<Shortcut name="Bulk action: Cancel runs">
|
||||
<ShortcutKey shortcut={{ key: "c" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Bulk action: Replay runs">
|
||||
<ShortcutKey shortcut={{ key: "r" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Bulk action: Clear selection">
|
||||
<ShortcutKey shortcut={{ key: "esc" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<Header3>Run page</Header3>
|
||||
<Shortcut name="Replay run">
|
||||
<ShortcutKey shortcut={{ key: "r" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Overview">
|
||||
<ShortcutKey shortcut={{ key: "o" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Details">
|
||||
<ShortcutKey shortcut={{ key: "d" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Context">
|
||||
<ShortcutKey shortcut={{ key: "c" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Metadata">
|
||||
<ShortcutKey shortcut={{ key: "m" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Navigate">
|
||||
<ShortcutKey shortcut={{ key: "arrowup" }} variant="medium/bright" />
|
||||
<ShortcutKey shortcut={{ key: "arrowdown" }} variant="medium/bright" />
|
||||
<ShortcutKey shortcut={{ key: "arrowleft" }} variant="medium/bright" />
|
||||
<ShortcutKey shortcut={{ key: "arrowright" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Expand all">
|
||||
<ShortcutKey shortcut={{ key: "e" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Collapse all">
|
||||
<ShortcutKey shortcut={{ key: "w" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Toggle level">
|
||||
<ShortcutKey shortcut={{ key: "0" }} variant="medium/bright" />
|
||||
<Paragraph variant="small" className="ml-1.5">
|
||||
to
|
||||
</Paragraph>
|
||||
<ShortcutKey shortcut={{ key: "9" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<Header3>Schedules page</Header3>
|
||||
<Shortcut name="New schedule">
|
||||
<ShortcutKey shortcut={{ key: "n" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<Header3>Alerts page</Header3>
|
||||
<Shortcut name="New alert">
|
||||
<ShortcutKey shortcut={{ key: "n" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
</div>
|
||||
</div>
|
||||
</SheetHeader>
|
||||
</SheetContent>
|
||||
);
|
||||
}
|
||||
|
||||
function Shortcut({ children, name }: { children: React.ReactNode; name: string }) {
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-x-2">
|
||||
|
||||
@@ -2,9 +2,9 @@ import {
|
||||
ArrowUpRightIcon,
|
||||
BookOpenIcon,
|
||||
CalendarDaysIcon,
|
||||
ChatBubbleLeftEllipsisIcon,
|
||||
EnvelopeIcon,
|
||||
LightBulbIcon,
|
||||
QuestionMarkCircleIcon,
|
||||
SignalIcon,
|
||||
StarIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
@@ -12,6 +12,7 @@ import { DiscordIcon, SlackIcon } from "@trigger.dev/companyicons";
|
||||
import { Fragment, useState } from "react";
|
||||
import { useCurrentPlan } from "~/routes/_app.orgs.$organizationSlug/route";
|
||||
import { Feedback } from "../Feedback";
|
||||
import { Shortcuts } from "../Shortcuts";
|
||||
import { StepContentContainer } from "../StepContentContainer";
|
||||
import { Button } from "../primitives/Buttons";
|
||||
import { ClipboardField } from "../primitives/ClipboardField";
|
||||
@@ -21,16 +22,21 @@ import { Paragraph } from "../primitives/Paragraph";
|
||||
import { Popover, PopoverContent, PopoverSideMenuTrigger } from "../primitives/Popover";
|
||||
import { StepNumber } from "../primitives/StepNumber";
|
||||
import { MenuCount, SideMenuItem } from "./SideMenuItem";
|
||||
import { Shortcuts } from "../Shortcuts";
|
||||
export function HelpAndFeedback() {
|
||||
|
||||
export function HelpAndFeedback({ disableShortcut = false }: { disableShortcut?: boolean }) {
|
||||
const [isHelpMenuOpen, setHelpMenuOpen] = useState(false);
|
||||
const currentPlan = useCurrentPlan();
|
||||
|
||||
return (
|
||||
<Popover onOpenChange={(open) => setHelpMenuOpen(open)}>
|
||||
<PopoverSideMenuTrigger isOpen={isHelpMenuOpen} shortcut={{ key: "h" }}>
|
||||
<PopoverSideMenuTrigger
|
||||
isOpen={isHelpMenuOpen}
|
||||
shortcut={{ key: "h", enabledOnInputElements: false }}
|
||||
className="grow pr-2"
|
||||
disabled={disableShortcut}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<ChatBubbleLeftEllipsisIcon className="size-4 text-success" />
|
||||
<QuestionMarkCircleIcon className="size-4 text-success" />
|
||||
Help & Feedback
|
||||
</div>
|
||||
</PopoverSideMenuTrigger>
|
||||
|
||||
@@ -17,9 +17,10 @@ import {
|
||||
ServerStackIcon,
|
||||
Squares2X2Icon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { useNavigation } from "@remix-run/react";
|
||||
import { useLocation, useNavigation } from "@remix-run/react";
|
||||
import { useEffect, useRef, useState, type ReactNode } from "react";
|
||||
import simplur from "simplur";
|
||||
import { AISparkleIcon } from "~/assets/icons/AISparkleIcon";
|
||||
import { RunsIconExtraSmall } from "~/assets/icons/RunsIcon";
|
||||
import { TaskIconSmall } from "~/assets/icons/TaskIcon";
|
||||
import { WaitpointTokenIcon } from "~/assets/icons/WaitpointTokenIcon";
|
||||
@@ -55,6 +56,7 @@ import {
|
||||
v3UsagePath,
|
||||
v3WaitpointTokensPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { useKapaWidget } from "../../hooks/useKapaWidget";
|
||||
import { FreePlanUsage } from "../billing/FreePlanUsage";
|
||||
import { ConnectionIcon, DevPresencePanel, useDevPresence } from "../DevPresence";
|
||||
import { ImpersonationBanner } from "../ImpersonationBanner";
|
||||
@@ -68,8 +70,10 @@ import {
|
||||
PopoverMenuItem,
|
||||
PopoverTrigger,
|
||||
} from "../primitives/Popover";
|
||||
import { ShortcutKey } from "../primitives/ShortcutKey";
|
||||
import { TextLink } from "../primitives/TextLink";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../primitives/Tooltip";
|
||||
import { ShortcutsAutoOpen } from "../Shortcuts";
|
||||
import { UserProfilePhoto } from "../UserProfilePhoto";
|
||||
import { EnvironmentSelector } from "./EnvironmentSelector";
|
||||
import { HelpAndFeedback } from "./HelpAndFeedbackPopover";
|
||||
@@ -276,7 +280,9 @@ export function SideMenu({
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 border-t border-grid-bright p-1">
|
||||
<HelpAndFeedback />
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<HelpAndAI />
|
||||
</div>
|
||||
{isFreeUser && (
|
||||
<FreePlanUsage
|
||||
to={v3BillingPath(organization)}
|
||||
@@ -538,3 +544,43 @@ function SelectorDivider() {
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function HelpAndAI() {
|
||||
const { isKapaEnabled, isKapaOpen, openKapa } = useKapaWidget();
|
||||
|
||||
return (
|
||||
<>
|
||||
<ShortcutsAutoOpen />
|
||||
<HelpAndFeedback disableShortcut={isKapaOpen} />
|
||||
{isKapaEnabled && (
|
||||
<TooltipProvider disableHoverableContent>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="inline-flex">
|
||||
<Button
|
||||
variant="small-menu-item"
|
||||
data-action="ask-ai"
|
||||
shortcut={{ modifiers: ["mod"], key: "/", enabledOnInputElements: true }}
|
||||
hideShortcutKey
|
||||
data-modal-override-open-class-ask-ai="true"
|
||||
onClick={() => {
|
||||
openKapa();
|
||||
}}
|
||||
>
|
||||
<AISparkleIcon className="size-5" />
|
||||
</Button>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="top"
|
||||
className="flex items-center gap-1 py-1.5 pl-2.5 pr-2 text-xs"
|
||||
>
|
||||
Ask AI
|
||||
<ShortcutKey shortcut={{ modifiers: ["mod"], key: "/" }} variant="medium/bright" />
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -177,6 +177,7 @@ export type ButtonContentPropsType = {
|
||||
shortcutPosition?: "before-trailing-icon" | "after-trailing-icon";
|
||||
tooltip?: ReactNode;
|
||||
iconSpacing?: string;
|
||||
hideShortcutKey?: boolean;
|
||||
};
|
||||
|
||||
export function ButtonContent(props: ButtonContentPropsType) {
|
||||
@@ -192,6 +193,7 @@ export function ButtonContent(props: ButtonContentPropsType) {
|
||||
className,
|
||||
tooltip,
|
||||
iconSpacing,
|
||||
hideShortcutKey,
|
||||
} = props;
|
||||
const variation = allVariants.variant[props.variant];
|
||||
|
||||
@@ -202,7 +204,8 @@ export function ButtonContent(props: ButtonContentPropsType) {
|
||||
const textColorClassName = variation.textColor;
|
||||
|
||||
const renderShortcutKey = () =>
|
||||
shortcut && (
|
||||
shortcut &&
|
||||
!hideShortcutKey && (
|
||||
<ShortcutKey
|
||||
className={cn(shortcutClassName)}
|
||||
shortcut={shortcut}
|
||||
|
||||
@@ -3,9 +3,10 @@ import { KeyboardLeftIcon } from "~/assets/icons/KeyboardLeftIcon";
|
||||
import { KeyboardRightIcon } from "~/assets/icons/KeyboardRightIcon";
|
||||
import { KeyboardUpIcon } from "~/assets/icons/KeyboardUpIcon";
|
||||
import { KeyboardWindowsIcon } from "~/assets/icons/KeyboardWindowsIcon";
|
||||
import { Modifier, Shortcut } from "~/hooks/useShortcutKeys";
|
||||
import { type Modifier, type Shortcut } from "~/hooks/useShortcutKeys";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { useOperatingSystem } from "./OperatingSystemProvider";
|
||||
import { KeyboardEnterIcon } from "~/assets/icons/KeyboardEnterIcon";
|
||||
|
||||
const medium =
|
||||
"text-[0.75rem] font-medium min-w-[17px] rounded-[2px] tabular-nums px-1 ml-1 -mr-0.5 flex items-center gap-x-1.5 border border-dimmed/40 text-text-dimmed group-hover:text-text-bright/80 group-hover:border-dimmed/60 transition uppercase";
|
||||
@@ -60,7 +61,11 @@ function keyString(key: string, isMac: boolean, variant: "small" | "medium" | "m
|
||||
|
||||
switch (key) {
|
||||
case "enter":
|
||||
return isMac ? "↵" : <span className="capitalize">Enter</span>;
|
||||
return isMac ? (
|
||||
<KeyboardEnterIcon className={className} />
|
||||
) : (
|
||||
<span className="capitalize">Enter</span>
|
||||
);
|
||||
case "esc":
|
||||
return <span className="capitalize">Esc</span>;
|
||||
case "del":
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { createContext, useContext, useState, useCallback, useMemo, type ReactNode } from "react";
|
||||
|
||||
type ShortcutsContextType = {
|
||||
areShortcutsEnabled: boolean;
|
||||
disableShortcuts: () => void;
|
||||
enableShortcuts: () => void;
|
||||
};
|
||||
|
||||
const ShortcutsContext = createContext<ShortcutsContextType | null>(null);
|
||||
|
||||
type ShortcutsProviderProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export function ShortcutsProvider({ children }: ShortcutsProviderProps) {
|
||||
const [areShortcutsEnabled, setAreShortcutsEnabled] = useState(true);
|
||||
|
||||
const disableShortcuts = useCallback(() => setAreShortcutsEnabled(false), []);
|
||||
const enableShortcuts = useCallback(() => setAreShortcutsEnabled(true), []);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
areShortcutsEnabled,
|
||||
disableShortcuts,
|
||||
enableShortcuts,
|
||||
}),
|
||||
[areShortcutsEnabled, disableShortcuts, enableShortcuts]
|
||||
);
|
||||
|
||||
return <ShortcutsContext.Provider value={value}>{children}</ShortcutsContext.Provider>;
|
||||
}
|
||||
|
||||
const throwIfNoProvider = () => {
|
||||
throw new Error("useShortcuts must be used within a ShortcutsProvider");
|
||||
};
|
||||
|
||||
export const useShortcuts = () => {
|
||||
return useContext(ShortcutsContext) ?? throwIfNoProvider();
|
||||
};
|
||||
@@ -714,6 +714,9 @@ const EnvironmentSchema = z.object({
|
||||
|
||||
QUEUE_SSE_AUTORELOAD_INTERVAL_MS: z.coerce.number().int().default(5_000),
|
||||
QUEUE_SSE_AUTORELOAD_TIMEOUT_MS: z.coerce.number().int().default(60_000),
|
||||
|
||||
// kapa.ai
|
||||
KAPA_AI_WEBSITE_ID: z.string().optional(),
|
||||
});
|
||||
|
||||
export type Environment = z.infer<typeof EnvironmentSchema>;
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import { useMatches, useSearchParams } from "@remix-run/react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useFeatures } from "~/hooks/useFeatures";
|
||||
import { type loader } from "~/root";
|
||||
import { useShortcuts } from "../components/primitives/ShortcutsProvider";
|
||||
import { useTypedMatchesData } from "./useTypedMatchData";
|
||||
|
||||
type OpenOptions = { mode: string; query: string; submit: boolean };
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
Kapa: (
|
||||
command: string,
|
||||
options?: (() => void) | { onRender?: () => void } | OpenOptions,
|
||||
remove?: string | { onRender?: () => void }
|
||||
) => void;
|
||||
}
|
||||
}
|
||||
|
||||
export function KapaScripts({ websiteId }: { websiteId?: string }) {
|
||||
if (!websiteId) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<script
|
||||
async
|
||||
src="https://widget.kapa.ai/kapa-widget.bundle.js"
|
||||
data-website-id={websiteId}
|
||||
data-project-name={"Trigger.dev"}
|
||||
data-project-color={"#6366F1"}
|
||||
data-project-logo={"https://content.trigger.dev/trigger-logo-circle.png"}
|
||||
data-render-on-load={"false"}
|
||||
data-button-hide={"true"}
|
||||
data-modal-disclaimer-bg-color={"#D7D9DD"}
|
||||
data-modal-header-bg-color={"#2C3034"}
|
||||
data-modal-title-color={"#D7D9DD"}
|
||||
></script>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function () {
|
||||
let k = window.Kapa;
|
||||
if (!k) {
|
||||
let i = function () {
|
||||
i.c(arguments);
|
||||
};
|
||||
i.q = [];
|
||||
i.c = function (args) {
|
||||
i.q.push(args);
|
||||
};
|
||||
window.Kapa = i;
|
||||
}
|
||||
})();
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function useKapaConfig() {
|
||||
const matches = useMatches();
|
||||
const routeMatch = useTypedMatchesData<typeof loader>({
|
||||
id: "root",
|
||||
matches,
|
||||
});
|
||||
return routeMatch?.kapa;
|
||||
}
|
||||
|
||||
export function useKapaWidget() {
|
||||
const kapa = useKapaConfig();
|
||||
const features = useFeatures();
|
||||
const { disableShortcuts, enableShortcuts, areShortcutsEnabled } = useShortcuts();
|
||||
const [isKapaOpen, setIsKapaOpen] = useState(false);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const handleModalClose = useCallback(() => {
|
||||
setIsKapaOpen(false);
|
||||
enableShortcuts();
|
||||
}, [enableShortcuts]);
|
||||
|
||||
const handleModalOpen = useCallback(() => {
|
||||
setIsKapaOpen(true);
|
||||
disableShortcuts();
|
||||
}, [disableShortcuts]);
|
||||
|
||||
// Handle opening/closing
|
||||
useEffect(() => {
|
||||
if (!features.isManagedCloud || !kapa?.websiteId) return;
|
||||
|
||||
window.Kapa("render", {
|
||||
onRender: () => {
|
||||
//this auto-opens the Kapa widget on page load if the query contains ?aiHelp=
|
||||
const aiHelp = searchParams.get("aiHelp");
|
||||
if (aiHelp) {
|
||||
setSearchParams((prev) => {
|
||||
prev.delete("aiHelp");
|
||||
return prev;
|
||||
});
|
||||
|
||||
//we need to decode the aiHelp string because it's urlencoded
|
||||
const decodedAiHelp = decodeURIComponent(aiHelp);
|
||||
//sadly this delay is required, otherwise it fails the Kapa browser verification…
|
||||
setTimeout(() => {
|
||||
openKapa(decodedAiHelp);
|
||||
}, 500);
|
||||
}
|
||||
},
|
||||
});
|
||||
window.Kapa("onModalOpen", handleModalOpen);
|
||||
window.Kapa("onModalClose", handleModalClose);
|
||||
|
||||
return () => {
|
||||
window.Kapa("onModalOpen", handleModalOpen, "remove");
|
||||
window.Kapa("onModalClose", handleModalClose, "remove");
|
||||
};
|
||||
}, [features.isManagedCloud, kapa?.websiteId, searchParams, setSearchParams]);
|
||||
|
||||
// Handle opening the Kapa widget
|
||||
const openKapa = useCallback(
|
||||
(query?: string) => {
|
||||
if (!features.isManagedCloud || !kapa?.websiteId) return;
|
||||
|
||||
window.Kapa(
|
||||
"open",
|
||||
query
|
||||
? {
|
||||
mode: "ai",
|
||||
query,
|
||||
submit: true,
|
||||
}
|
||||
: undefined
|
||||
);
|
||||
setIsKapaOpen(true);
|
||||
disableShortcuts();
|
||||
},
|
||||
[disableShortcuts, features.isManagedCloud, kapa?.websiteId]
|
||||
);
|
||||
|
||||
return {
|
||||
isKapaEnabled: features.isManagedCloud && kapa?.websiteId,
|
||||
openKapa,
|
||||
isKapaOpen,
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { useOperatingSystem } from "~/components/primitives/OperatingSystemProvider";
|
||||
import { useShortcuts } from "~/components/primitives/ShortcutsProvider";
|
||||
|
||||
export type Modifier = "alt" | "ctrl" | "meta" | "shift" | "mod";
|
||||
|
||||
@@ -7,6 +8,7 @@ export type Shortcut = {
|
||||
key: string;
|
||||
modifiers?: Modifier[];
|
||||
enabledOnInputElements?: boolean;
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
export type ShortcutDefinition =
|
||||
@@ -30,20 +32,26 @@ export function useShortcutKeys({
|
||||
enabledOnInputElements,
|
||||
}: useShortcutKeysProps) {
|
||||
const { platform } = useOperatingSystem();
|
||||
const { areShortcutsEnabled } = useShortcuts();
|
||||
const isMac = platform === "mac";
|
||||
const relevantShortcut =
|
||||
shortcut && "mac" in shortcut ? (isMac ? shortcut.mac : shortcut.windows) : shortcut;
|
||||
|
||||
const keys = createKeysFromShortcut(relevantShortcut);
|
||||
|
||||
const isEnabled = !disabled && areShortcutsEnabled && relevantShortcut?.enabled !== false;
|
||||
|
||||
useHotkeys(
|
||||
keys,
|
||||
(event, hotkeysEvent) => {
|
||||
action(event);
|
||||
},
|
||||
{
|
||||
enabled: !disabled,
|
||||
enableOnFormTags: enabledOnInputElements ?? relevantShortcut?.enabledOnInputElements,
|
||||
enableOnContentEditable: enabledOnInputElements ?? relevantShortcut?.enabledOnInputElements,
|
||||
enabled: isEnabled,
|
||||
enableOnFormTags:
|
||||
isEnabled && (enabledOnInputElements ?? relevantShortcut?.enabledOnInputElements),
|
||||
enableOnContentEditable:
|
||||
isEnabled && (enabledOnInputElements ?? relevantShortcut?.enabledOnInputElements),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
+27
-10
@@ -1,19 +1,30 @@
|
||||
import type { LinksFunction, LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
|
||||
import type { ShouldRevalidateFunction } from "@remix-run/react";
|
||||
import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration } from "@remix-run/react";
|
||||
import { UseDataFunctionReturn, typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import {
|
||||
Links,
|
||||
LiveReload,
|
||||
Meta,
|
||||
Outlet,
|
||||
Scripts,
|
||||
ScrollRestoration,
|
||||
useMatches,
|
||||
} from "@remix-run/react";
|
||||
import { type UseDataFunctionReturn, typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { ExternalScripts } from "remix-utils/external-scripts";
|
||||
import type { ToastMessage } from "~/models/message.server";
|
||||
import { commitSession, getSession } from "~/models/message.server";
|
||||
import tailwindStylesheetUrl from "~/tailwind.css";
|
||||
import { RouteErrorDisplay } from "./components/ErrorDisplay";
|
||||
import { AppContainer, MainCenteredContainer } from "./components/layout/AppLayout";
|
||||
import { ShortcutsProvider } from "./components/primitives/ShortcutsProvider";
|
||||
import { Toast } from "./components/primitives/Toast";
|
||||
import { env } from "./env.server";
|
||||
import { featuresForRequest } from "./features.server";
|
||||
import { usePostHog } from "./hooks/usePostHog";
|
||||
import { useTypedMatchesData } from "./hooks/useTypedMatchData";
|
||||
import { getUser } from "./services/session.server";
|
||||
import { appEnvTitleTag } from "./utils";
|
||||
import { KapaScripts } from "./hooks/useKapaWidget";
|
||||
|
||||
export const links: LinksFunction = () => {
|
||||
return [{ rel: "stylesheet", href: tailwindStylesheetUrl }];
|
||||
@@ -40,6 +51,10 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
const posthogProjectKey = env.POSTHOG_PROJECT_KEY;
|
||||
const features = featuresForRequest(request);
|
||||
|
||||
const kapa = {
|
||||
websiteId: env.KAPA_AI_WEBSITE_ID,
|
||||
};
|
||||
|
||||
return typedjson(
|
||||
{
|
||||
user: await getUser(request),
|
||||
@@ -48,6 +63,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
features,
|
||||
appEnv: env.APP_ENV,
|
||||
appOrigin: env.APP_ORIGIN,
|
||||
kapa,
|
||||
},
|
||||
{ headers: { "Set-Cookie": await commitSession(session) } }
|
||||
);
|
||||
@@ -73,7 +89,7 @@ export function ErrorBoundary() {
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body className="bg-darkBackground h-full overflow-hidden">
|
||||
<body className="h-full overflow-hidden bg-background-dimmed">
|
||||
<AppContainer>
|
||||
<MainCenteredContainer>
|
||||
<RouteErrorDisplay />
|
||||
@@ -86,8 +102,8 @@ export function ErrorBoundary() {
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
const { posthogProjectKey } = useTypedLoaderData<typeof loader>();
|
||||
export default function App() {
|
||||
const { posthogProjectKey, kapa } = useTypedLoaderData<typeof loader>();
|
||||
usePostHog(posthogProjectKey);
|
||||
|
||||
return (
|
||||
@@ -96,10 +112,13 @@ function App() {
|
||||
<head>
|
||||
<Meta />
|
||||
<Links />
|
||||
<KapaScripts websiteId={kapa.websiteId} />
|
||||
</head>
|
||||
<body className="bg-darkBackground h-full overflow-hidden">
|
||||
<Outlet />
|
||||
<Toast />
|
||||
<body className="h-full overflow-hidden bg-background-dimmed">
|
||||
<ShortcutsProvider>
|
||||
<Outlet />
|
||||
<Toast />
|
||||
</ShortcutsProvider>
|
||||
<ScrollRestoration />
|
||||
<ExternalScripts />
|
||||
<Scripts />
|
||||
@@ -109,5 +128,3 @@ function App() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { type LoaderFunctionArgs, redirect } from "@remix-run/server-runtime";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import { env } from "~/env.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { v3EnvironmentPath, v3ProjectPath, v3TestPath } from "~/utils/pathBuilder";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
projectRef: z.string(),
|
||||
});
|
||||
|
||||
export async function loader({ params, request }: LoaderFunctionArgs) {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
const validatedParams = ParamsSchema.parse(params);
|
||||
|
||||
const project = await prisma.project.findFirst({
|
||||
where: {
|
||||
externalRef: validatedParams.projectRef,
|
||||
organization: {
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
organization: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
return new Response("Not found", { status: 404 });
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
const query = url.searchParams.get("q");
|
||||
|
||||
if (!query) {
|
||||
return new Response("No query", { status: 404 });
|
||||
}
|
||||
|
||||
const newUrl = new URL(
|
||||
v3EnvironmentPath({ slug: project.organization.slug }, { slug: project.slug }, { slug: "dev" }),
|
||||
env.LOGIN_ORIGIN
|
||||
);
|
||||
newUrl.searchParams.set("aiHelp", query);
|
||||
|
||||
return redirect(newUrl.toString());
|
||||
}
|
||||
+5
-3
@@ -83,14 +83,14 @@ export function AIGeneratedCronField({ onSuccess }: AIGeneratedCronFieldProps) {
|
||||
return (
|
||||
<div className="max-w-md">
|
||||
<Label>
|
||||
<AISparkleIcon className="inline-block h-4 w-4" /> Describe your schedule using natural
|
||||
<AISparkleIcon className="inline-block size-4" /> Describe your schedule using natural
|
||||
language
|
||||
</Label>
|
||||
<div
|
||||
className="rounded-sm p-px"
|
||||
className="mt-1 rounded p-px"
|
||||
style={{ background: "linear-gradient(to bottom right, #E543FF, #286399)" }}
|
||||
>
|
||||
<div className="rounded-[calc(0.5rem-2px)] bg-background-bright">
|
||||
<div className="overflow-hidden rounded-sm bg-background-bright">
|
||||
<textarea
|
||||
value={text}
|
||||
placeholder="e.g. the last Friday of the month at 6am"
|
||||
@@ -105,6 +105,8 @@ export function AIGeneratedCronField({ onSuccess }: AIGeneratedCronFieldProps) {
|
||||
disabled={isLoading}
|
||||
LeadingIcon={isLoading ? Spinner : AISparkleIcon}
|
||||
onClick={() => submit(text)}
|
||||
className="pl-1.5"
|
||||
iconSpacing="gap-1.5"
|
||||
>
|
||||
{isLoading ? "Generating" : "Generate"}
|
||||
</Button>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useState } from "react";
|
||||
import { Button } from "~/components/primitives/Buttons";
|
||||
import { Header1 } from "~/components/primitives/Headers";
|
||||
import { OperatingSystemContextProvider } from "~/components/primitives/OperatingSystemProvider";
|
||||
import { ShortcutKey } from "~/components/primitives/ShortcutKey";
|
||||
import { ShortcutDefinition } from "~/hooks/useShortcutKeys";
|
||||
import { useShortcuts } from "~/components/primitives/ShortcutsProvider";
|
||||
import { type ShortcutDefinition } from "~/hooks/useShortcutKeys";
|
||||
|
||||
const shortcuts: ShortcutDefinition[] = [
|
||||
{ key: "esc" },
|
||||
@@ -19,6 +21,10 @@ const shortcuts: ShortcutDefinition[] = [
|
||||
export default function Story() {
|
||||
return (
|
||||
<div className="flex flex-col items-start gap-y-4 p-12">
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<Header1 spacing>Enable/disable</Header1>
|
||||
<DisableTester />
|
||||
</div>
|
||||
<Collection platform="mac" />
|
||||
<Collection platform="windows" />
|
||||
</div>
|
||||
@@ -66,3 +72,33 @@ function Collection({ platform }: { platform: "mac" | "windows" }) {
|
||||
</OperatingSystemContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function DisableTester() {
|
||||
const { disableShortcuts, enableShortcuts, areShortcutsEnabled } = useShortcuts();
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<div className="flex items-center gap-x-4">
|
||||
<div>Shortcuts are: {areShortcutsEnabled ? "Enabled" : "Disabled"}</div>
|
||||
<Button
|
||||
variant="primary/small"
|
||||
onClick={() => (areShortcutsEnabled ? disableShortcuts() : enableShortcuts())}
|
||||
>
|
||||
{areShortcutsEnabled ? "Disable" : "Enable"} Shortcuts
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-x-4">
|
||||
<Button
|
||||
variant="secondary/medium"
|
||||
shortcut={{ key: "i" }}
|
||||
onClick={() => setCount((c) => c + 1)}
|
||||
>
|
||||
Increment Counter
|
||||
</Button>
|
||||
<div>Count: {count}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -51,6 +51,15 @@ export type BundleResult = {
|
||||
stop: (() => Promise<void>) | undefined;
|
||||
};
|
||||
|
||||
export class BundleError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly issues?: esbuild.Message[]
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
export async function bundleWorker(options: BundleOptions): Promise<BundleResult> {
|
||||
const { resolvedConfig } = options;
|
||||
|
||||
@@ -129,7 +138,7 @@ export async function bundleWorker(options: BundleOptions): Promise<BundleResult
|
||||
await currentContext.watch();
|
||||
result = await initialBuildResultPromise;
|
||||
if (result.errors.length > 0) {
|
||||
throw new Error("Failed to build");
|
||||
throw new BundleError("Failed to build", result.errors);
|
||||
}
|
||||
|
||||
stop = async function () {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { outro } from "@clack/prompts";
|
||||
import { chalkError } from "../utilities/cliOutput.js";
|
||||
import { CLOUD_API_URL } from "../consts.js";
|
||||
import { readAuthConfigCurrentProfileName } from "../utilities/configFiles.js";
|
||||
import { BundleError } from "../build/bundle.js";
|
||||
|
||||
export const CommonCommandOptions = z.object({
|
||||
apiUrl: z.string().optional(),
|
||||
@@ -54,7 +55,7 @@ export async function wrapCommandAction<T extends z.AnyZodObject, TResult>(
|
||||
schema: T,
|
||||
options: unknown,
|
||||
action: (opts: z.output<T>) => Promise<TResult>
|
||||
): Promise<TResult> {
|
||||
): Promise<TResult | undefined> {
|
||||
return await tracer.startActiveSpan(name, async (span) => {
|
||||
try {
|
||||
const parsedOptions = schema.safeParse(options);
|
||||
@@ -86,6 +87,8 @@ export async function wrapCommandAction<T extends z.AnyZodObject, TResult>(
|
||||
outro("Operation cancelled");
|
||||
} else if (e instanceof SkipCommandError) {
|
||||
// do nothing
|
||||
} else if (e instanceof BundleError) {
|
||||
process.exit(1);
|
||||
} else {
|
||||
recordSpanException(span, e);
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { ResolvedConfig } from "@trigger.dev/core/v3/build";
|
||||
import { Command } from "commander";
|
||||
import { z } from "zod";
|
||||
import { CommonCommandOptions, commonOptions, wrapCommandAction } from "../cli/common.js";
|
||||
import {
|
||||
CommonCommandOptions,
|
||||
commonOptions,
|
||||
SkipLoggingError,
|
||||
wrapCommandAction,
|
||||
} from "../cli/common.js";
|
||||
import { watchConfig } from "../config.js";
|
||||
import { DevSessionInstance, startDevSession } from "../dev/devSession.js";
|
||||
import { chalkError } from "../utilities/cliOutput.js";
|
||||
@@ -12,6 +17,7 @@ import { getProjectClient, LoginResultOk } from "../utilities/session.js";
|
||||
import { login } from "./login.js";
|
||||
import { updateTriggerPackages } from "./update.js";
|
||||
import { createLockFile } from "../dev/lock.js";
|
||||
import { BundleError } from "../build/bundle.js";
|
||||
|
||||
const DevCommandOptions = CommonCommandOptions.extend({
|
||||
debugOtel: z.boolean().default(false),
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { TaskRunError, TaskRunErrorCodes } from "@trigger.dev/core/v3/schemas";
|
||||
import { DevCommandOptions } from "../commands/dev.js";
|
||||
import {
|
||||
aiHelpLink,
|
||||
chalkError,
|
||||
chalkGrey,
|
||||
chalkLink,
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
import { eventBus, EventBusEventArgs } from "../utilities/eventBus.js";
|
||||
import { logger } from "../utilities/logger.js";
|
||||
import { Socket } from "socket.io-client";
|
||||
import { BundleError } from "../build/bundle.js";
|
||||
|
||||
export type DevOutputOptions = {
|
||||
name: string | undefined;
|
||||
@@ -45,6 +47,23 @@ export function startDevOutput(options: DevOutputOptions) {
|
||||
logger.log(chalkGrey("○ Building background worker…"));
|
||||
};
|
||||
|
||||
const buildFailed = (...[target, error]: EventBusEventArgs<"buildFailed">) => {
|
||||
const errorText = error instanceof Error ? error.message : "Unknown error";
|
||||
const stack = error instanceof Error ? error.stack : undefined;
|
||||
|
||||
let issues: string[] = [];
|
||||
|
||||
if (error instanceof BundleError) {
|
||||
issues = error.issues?.map((issue) => `${issue.text} (${issue.location?.file})`) ?? [];
|
||||
}
|
||||
|
||||
aiHelpLink({
|
||||
dashboardUrl,
|
||||
project: config.project,
|
||||
query: `Build failed:\n ${errorText}\n${issues.join("\n")}\n${stack}`,
|
||||
});
|
||||
};
|
||||
|
||||
const workerSkipped = () => {
|
||||
logger.log(chalkGrey("○ No changes detected, skipping build…"));
|
||||
};
|
||||
@@ -83,12 +102,18 @@ export function startDevOutput(options: DevOutputOptions) {
|
||||
...[buildManifest, error]: EventBusEventArgs<"backgroundWorkerIndexingError">
|
||||
) => {
|
||||
if (error instanceof TaskIndexingImportError) {
|
||||
let errorText = "";
|
||||
for (const importError of error.importErrors) {
|
||||
prettyError(
|
||||
`Could not import ${importError.file}`,
|
||||
importError.stack ?? importError.message
|
||||
);
|
||||
errorText += `Could not import ${importError.file}:\n ${
|
||||
importError.stack ?? importError.message
|
||||
}\n`;
|
||||
}
|
||||
|
||||
aiHelpLink({ dashboardUrl, project: config.project, query: errorText });
|
||||
} else if (error instanceof TaskMetadataParseError) {
|
||||
const errorStack = createTaskMetadataFailedErrorStack({
|
||||
version: "v1",
|
||||
@@ -97,11 +122,21 @@ export function startDevOutput(options: DevOutputOptions) {
|
||||
});
|
||||
|
||||
prettyError(`Could not parse task metadata`, errorStack);
|
||||
aiHelpLink({
|
||||
dashboardUrl,
|
||||
project: config.project,
|
||||
query: `Could not parse task metadata:\n ${errorStack}`,
|
||||
});
|
||||
} else {
|
||||
const errorText = error instanceof Error ? error.message : "Unknown error";
|
||||
const stack = error instanceof Error ? error.stack : undefined;
|
||||
|
||||
prettyError(`Build failed: ${errorText}`, stack);
|
||||
aiHelpLink({
|
||||
dashboardUrl,
|
||||
project: config.project,
|
||||
query: `Build failed:\n ${errorText}\n${stack}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -184,6 +219,7 @@ export function startDevOutput(options: DevOutputOptions) {
|
||||
|
||||
eventBus.on("rebuildStarted", rebuildStarted);
|
||||
eventBus.on("buildStarted", buildStarted);
|
||||
eventBus.on("buildFailed", buildFailed);
|
||||
eventBus.on("workerSkipped", workerSkipped);
|
||||
eventBus.on("backgroundWorkerInitialized", backgroundWorkerInitialized);
|
||||
eventBus.on("runStarted", runStarted);
|
||||
@@ -195,6 +231,7 @@ export function startDevOutput(options: DevOutputOptions) {
|
||||
return () => {
|
||||
eventBus.off("rebuildStarted", rebuildStarted);
|
||||
eventBus.off("buildStarted", buildStarted);
|
||||
eventBus.off("buildFailed", buildFailed);
|
||||
eventBus.off("workerSkipped", workerSkipped);
|
||||
eventBus.off("backgroundWorkerInitialized", backgroundWorkerInitialized);
|
||||
eventBus.off("runStarted", runStarted);
|
||||
|
||||
@@ -24,6 +24,7 @@ import { clearTmpDirs, EphemeralDirectory, getTmpDir } from "../utilities/tempDi
|
||||
import { startDevOutput } from "./devOutput.js";
|
||||
import { startWorkerRuntime } from "./devSupervisor.js";
|
||||
import { startMcpServer, stopMcpServer } from "./mcpServer.js";
|
||||
import { aiHelpLink } from "../utilities/cliOutput.js";
|
||||
|
||||
export type DevSessionOptions = {
|
||||
name: string | undefined;
|
||||
@@ -172,24 +173,34 @@ export async function startDevSession({
|
||||
async function runBundle() {
|
||||
eventBus.emit("buildStarted", "dev");
|
||||
|
||||
// Use glob to find initial entryPoints
|
||||
// Use chokidar to watch for entryPoints changes (e.g. added or removed?)
|
||||
// When there is a change, update entryPoints and start a new build with watch: true
|
||||
const bundleResult = await bundleWorker({
|
||||
target: "dev",
|
||||
cwd: rawConfig.workingDir,
|
||||
destination: destination.path,
|
||||
watch: true,
|
||||
resolvedConfig: rawConfig,
|
||||
plugins: [...pluginsFromExtensions, onEnd],
|
||||
jsxFactory: rawConfig.build.jsx.factory,
|
||||
jsxFragment: rawConfig.build.jsx.fragment,
|
||||
jsxAutomatic: rawConfig.build.jsx.automatic,
|
||||
});
|
||||
try {
|
||||
// Use glob to find initial entryPoints
|
||||
// Use chokidar to watch for entryPoints changes (e.g. added or removed?)
|
||||
// When there is a change, update entryPoints and start a new build with watch: true
|
||||
const bundleResult = await bundleWorker({
|
||||
target: "dev",
|
||||
cwd: rawConfig.workingDir,
|
||||
destination: destination.path,
|
||||
watch: true,
|
||||
resolvedConfig: rawConfig,
|
||||
plugins: [...pluginsFromExtensions, onEnd],
|
||||
jsxFactory: rawConfig.build.jsx.factory,
|
||||
jsxFragment: rawConfig.build.jsx.fragment,
|
||||
jsxAutomatic: rawConfig.build.jsx.automatic,
|
||||
});
|
||||
|
||||
await updateBundle(bundleResult);
|
||||
await updateBundle(bundleResult);
|
||||
|
||||
return bundleResult.stop;
|
||||
return bundleResult.stop;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
eventBus.emit("buildFailed", "dev", error);
|
||||
} else {
|
||||
eventBus.emit("buildFailed", "dev", new Error(String(error)));
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const stopBundling = await runBundle();
|
||||
|
||||
@@ -116,6 +116,27 @@ export function prettyWarning(header: string, body?: string, footer?: string) {
|
||||
);
|
||||
}
|
||||
|
||||
export function aiHelpLink({
|
||||
dashboardUrl,
|
||||
project,
|
||||
query,
|
||||
}: {
|
||||
dashboardUrl: string;
|
||||
project: string;
|
||||
query: string;
|
||||
}) {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
//the max length for a URL is 1950 characters
|
||||
const clippedQuery = query.slice(0, 1950);
|
||||
|
||||
searchParams.set("q", clippedQuery);
|
||||
const url = new URL(`/projects/${project}/ai-help`, dashboardUrl);
|
||||
url.search = searchParams.toString();
|
||||
|
||||
log.message(chalkLink(cliLink("💡 Get a fix for this error using AI", url.toString())));
|
||||
}
|
||||
|
||||
export function cliLink(text: string, url: string, options?: TerminalLinkOptions) {
|
||||
return terminalLink(text, url, {
|
||||
fallback: (text, url) => `${text} ${url}`,
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Socket } from "socket.io-client";
|
||||
export type EventBusEvents = {
|
||||
rebuildStarted: [BuildTarget];
|
||||
buildStarted: [BuildTarget];
|
||||
buildFailed: [BuildTarget, Error];
|
||||
workerSkipped: [];
|
||||
backgroundWorkerInitialized: [BackgroundWorker];
|
||||
backgroundWorkerIndexingError: [BuildManifest, Error];
|
||||
|
||||
Generated
+160
-183
File diff suppressed because it is too large
Load Diff
@@ -3,10 +3,10 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"trigger.dev": "4.0.0-v4-beta.7"
|
||||
"trigger.dev": "workspace:*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@trigger.dev/sdk": "4.0.0-v4-beta.7"
|
||||
"@trigger.dev/sdk": "workspace:*"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "trigger dev"
|
||||
|
||||
Reference in New Issue
Block a user