mirror of
https://github.com/eliasstepanik/core.git
synced 2026-01-27 03:08:34 +00:00
feat: add copy
This commit is contained in:
parent
e358dadf5a
commit
7319930048
@ -1,4 +1,4 @@
|
|||||||
import { EllipsisVertical, Trash } from "lucide-react";
|
import { EllipsisVertical, Trash, Copy } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@ -18,6 +18,7 @@ import {
|
|||||||
} from "../ui/alert-dialog";
|
} from "../ui/alert-dialog";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useFetcher, useNavigate } from "@remix-run/react";
|
import { useFetcher, useNavigate } from "@remix-run/react";
|
||||||
|
import { toast } from "~/hooks/use-toast";
|
||||||
|
|
||||||
interface LogOptionsProps {
|
interface LogOptionsProps {
|
||||||
id: string;
|
id: string;
|
||||||
@ -40,6 +41,23 @@ export const LogOptions = ({ id }: LogOptionsProps) => {
|
|||||||
setDeleteDialogOpen(false);
|
setDeleteDialogOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(id);
|
||||||
|
toast({
|
||||||
|
title: "Copied",
|
||||||
|
description: "Episode ID copied to clipboard",
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to copy:", err);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to copy ID",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (deleteFetcher.state === "idle" && deleteFetcher.data?.success) {
|
if (deleteFetcher.state === "idle" && deleteFetcher.data?.success) {
|
||||||
navigate(`/home/inbox`);
|
navigate(`/home/inbox`);
|
||||||
@ -48,16 +66,26 @@ export const LogOptions = ({ id }: LogOptionsProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button
|
<div className="flex items-center gap-2">
|
||||||
variant="secondary"
|
<Button
|
||||||
size="sm"
|
variant="secondary"
|
||||||
className="gap-2 rounded"
|
size="sm"
|
||||||
onClick={(e) => {
|
className="gap-2 rounded"
|
||||||
setDeleteDialogOpen(true);
|
onClick={handleCopy}
|
||||||
}}
|
>
|
||||||
>
|
<Copy size={15} /> Copy ID
|
||||||
<Trash size={15} /> Delete
|
</Button>
|
||||||
</Button>
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
className="gap-2 rounded"
|
||||||
|
onClick={(e) => {
|
||||||
|
setDeleteDialogOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash size={15} /> Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { EllipsisVertical, RefreshCcw, Trash, Edit } from "lucide-react";
|
import { EllipsisVertical, RefreshCcw, Trash, Edit, Copy } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@ -19,6 +19,7 @@ import {
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useFetcher, useNavigate } from "@remix-run/react";
|
import { useFetcher, useNavigate } from "@remix-run/react";
|
||||||
import { EditSpaceDialog } from "./edit-space-dialog.client";
|
import { EditSpaceDialog } from "./edit-space-dialog.client";
|
||||||
|
import { toast } from "~/hooks/use-toast";
|
||||||
|
|
||||||
interface SpaceOptionsProps {
|
interface SpaceOptionsProps {
|
||||||
id: string;
|
id: string;
|
||||||
@ -64,6 +65,23 @@ export const SpaceOptions = ({ id, name, description }: SpaceOptionsProps) => {
|
|||||||
// revalidator.revalidate();
|
// revalidator.revalidate();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(id);
|
||||||
|
toast({
|
||||||
|
title: "Copied",
|
||||||
|
description: "Space ID copied to clipboard",
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to copy:", err);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to copy ID",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
@ -79,6 +97,11 @@ export const SpaceOptions = ({ id, name, description }: SpaceOptionsProps) => {
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
|
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem onClick={handleCopy}>
|
||||||
|
<Button variant="link" size="sm" className="gap-2 rounded">
|
||||||
|
<Copy size={15} /> Copy ID
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => setEditDialogOpen(true)}>
|
<DropdownMenuItem onClick={() => setEditDialogOpen(true)}>
|
||||||
<Button variant="link" size="sm" className="gap-2 rounded">
|
<Button variant="link" size="sm" className="gap-2 rounded">
|
||||||
<Edit size={15} /> Edit
|
<Edit size={15} /> Edit
|
||||||
|
|||||||
@ -2,3 +2,5 @@ export * from "./button";
|
|||||||
export * from "./tabs";
|
export * from "./tabs";
|
||||||
export * from "./input";
|
export * from "./input";
|
||||||
export * from "./scrollarea";
|
export * from "./scrollarea";
|
||||||
|
export * from "./toast";
|
||||||
|
export * from "./toaster";
|
||||||
|
|||||||
133
apps/webapp/app/components/ui/toast.tsx
Normal file
133
apps/webapp/app/components/ui/toast.tsx
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import { Cross2Icon } from "@radix-ui/react-icons";
|
||||||
|
import * as ToastPrimitives from "@radix-ui/react-toast";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { cn } from "../../lib/utils";
|
||||||
|
|
||||||
|
const ToastProvider = ToastPrimitives.Provider;
|
||||||
|
|
||||||
|
const ToastViewport = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Viewport
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:top-auto sm:right-0 sm:bottom-0 sm:flex-col md:max-w-[420px]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
|
||||||
|
|
||||||
|
const toastVariants = cva(
|
||||||
|
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-3 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "border bg-background text-foreground",
|
||||||
|
warning: "warning group border-warning bg-warning text-foreground",
|
||||||
|
success: "success group border-success bg-success text-foreground",
|
||||||
|
destructive:
|
||||||
|
"destructive group border-destructive bg-destructive text-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const Toast = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||||
|
VariantProps<typeof toastVariants>
|
||||||
|
>(({ className, variant, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<ToastPrimitives.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
toastVariants({ variant }),
|
||||||
|
className,
|
||||||
|
"shadow-1 rounded-md border-0 bg-gray-100 font-sans backdrop-blur-md",
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Toast.displayName = ToastPrimitives.Root.displayName;
|
||||||
|
|
||||||
|
const ToastAction = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Action
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"hover:bg-secondary focus:ring-ring group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors focus:ring-1 focus:outline-none disabled:pointer-events-none disabled:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
ToastAction.displayName = ToastPrimitives.Action.displayName;
|
||||||
|
|
||||||
|
const ToastClose = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Close
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"text-foreground/50 hover:text-foreground absolute top-1 right-1 rounded-md p-1 opacity-0 transition-opacity group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 focus:opacity-100 focus:ring-1 focus:outline-none group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
toast-close=""
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Cross2Icon className="h-4 w-4" />
|
||||||
|
</ToastPrimitives.Close>
|
||||||
|
));
|
||||||
|
ToastClose.displayName = ToastPrimitives.Close.displayName;
|
||||||
|
|
||||||
|
const ToastTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn("font-medium [&+div]:text-xs", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
ToastTitle.displayName = ToastPrimitives.Title.displayName;
|
||||||
|
|
||||||
|
const ToastDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("opacity-90", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
ToastDescription.displayName = ToastPrimitives.Description.displayName;
|
||||||
|
|
||||||
|
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
|
||||||
|
|
||||||
|
type ToastActionElement = React.ReactElement<typeof ToastAction>;
|
||||||
|
|
||||||
|
export {
|
||||||
|
type ToastProps,
|
||||||
|
type ToastActionElement,
|
||||||
|
ToastProvider,
|
||||||
|
ToastViewport,
|
||||||
|
Toast,
|
||||||
|
ToastTitle,
|
||||||
|
ToastDescription,
|
||||||
|
ToastClose,
|
||||||
|
ToastAction,
|
||||||
|
};
|
||||||
33
apps/webapp/app/components/ui/toaster.tsx
Normal file
33
apps/webapp/app/components/ui/toaster.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import {
|
||||||
|
Toast,
|
||||||
|
ToastClose,
|
||||||
|
ToastDescription,
|
||||||
|
ToastProvider,
|
||||||
|
ToastTitle,
|
||||||
|
ToastViewport,
|
||||||
|
} from "~/components/ui/toast";
|
||||||
|
import { useToast } from "~/hooks/use-toast";
|
||||||
|
|
||||||
|
export function Toaster() {
|
||||||
|
const { toasts } = useToast();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToastProvider>
|
||||||
|
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||||
|
return (
|
||||||
|
<Toast key={id} {...props}>
|
||||||
|
<div className="grid gap-1">
|
||||||
|
{title && <ToastTitle>{title}</ToastTitle>}
|
||||||
|
{description && (
|
||||||
|
<ToastDescription>{description}</ToastDescription>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{action}
|
||||||
|
<ToastClose />
|
||||||
|
</Toast>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<ToastViewport />
|
||||||
|
</ToastProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
186
apps/webapp/app/hooks/use-toast.ts
Normal file
186
apps/webapp/app/hooks/use-toast.ts
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import type { ToastActionElement, ToastProps } from "~/components/ui/toast";
|
||||||
|
|
||||||
|
const TOAST_LIMIT = 1;
|
||||||
|
const TOAST_REMOVE_DELAY = 1000000;
|
||||||
|
|
||||||
|
type ToasterToast = ToastProps & {
|
||||||
|
id: string;
|
||||||
|
title?: React.ReactNode;
|
||||||
|
description?: React.ReactNode;
|
||||||
|
action?: ToastActionElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
const actionTypes = {
|
||||||
|
ADD_TOAST: "ADD_TOAST",
|
||||||
|
UPDATE_TOAST: "UPDATE_TOAST",
|
||||||
|
DISMISS_TOAST: "DISMISS_TOAST",
|
||||||
|
REMOVE_TOAST: "REMOVE_TOAST",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
function genId() {
|
||||||
|
count = (count + 1) % Number.MAX_SAFE_INTEGER;
|
||||||
|
return count.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionType = typeof actionTypes;
|
||||||
|
|
||||||
|
type Action =
|
||||||
|
| {
|
||||||
|
type: ActionType["ADD_TOAST"];
|
||||||
|
toast: ToasterToast;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType["UPDATE_TOAST"];
|
||||||
|
toast: Partial<ToasterToast>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType["DISMISS_TOAST"];
|
||||||
|
toastId?: ToasterToast["id"];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType["REMOVE_TOAST"];
|
||||||
|
toastId?: ToasterToast["id"];
|
||||||
|
};
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
toasts: ToasterToast[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
||||||
|
|
||||||
|
const addToRemoveQueue = (toastId: string) => {
|
||||||
|
if (toastTimeouts.has(toastId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
toastTimeouts.delete(toastId);
|
||||||
|
dispatch({
|
||||||
|
type: "REMOVE_TOAST",
|
||||||
|
toastId: toastId,
|
||||||
|
});
|
||||||
|
}, TOAST_REMOVE_DELAY);
|
||||||
|
|
||||||
|
toastTimeouts.set(toastId, timeout);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const reducer = (state: State, action: Action): State => {
|
||||||
|
switch (action.type) {
|
||||||
|
case "ADD_TOAST":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||||
|
};
|
||||||
|
|
||||||
|
case "UPDATE_TOAST":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: state.toasts.map((t) =>
|
||||||
|
t.id === action.toast.id ? { ...t, ...action.toast } : t,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
case "DISMISS_TOAST": {
|
||||||
|
const { toastId } = action;
|
||||||
|
|
||||||
|
if (toastId) {
|
||||||
|
addToRemoveQueue(toastId);
|
||||||
|
} else {
|
||||||
|
state.toasts.forEach((toast) => {
|
||||||
|
addToRemoveQueue(toast.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: state.toasts.map((t) =>
|
||||||
|
t.id === toastId || toastId === undefined
|
||||||
|
? {
|
||||||
|
...t,
|
||||||
|
open: false,
|
||||||
|
}
|
||||||
|
: t,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "REMOVE_TOAST":
|
||||||
|
if (action.toastId === undefined) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const listeners: Array<(state: State) => void> = [];
|
||||||
|
|
||||||
|
let memoryState: State = { toasts: [] };
|
||||||
|
|
||||||
|
function dispatch(action: Action) {
|
||||||
|
memoryState = reducer(memoryState, action);
|
||||||
|
listeners.forEach((listener) => {
|
||||||
|
listener(memoryState);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
type Toast = Omit<ToasterToast, "id">;
|
||||||
|
|
||||||
|
function toast({ ...props }: Toast) {
|
||||||
|
const id = genId();
|
||||||
|
|
||||||
|
const update = (props: ToasterToast) =>
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_TOAST",
|
||||||
|
toast: { ...props, id },
|
||||||
|
});
|
||||||
|
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "ADD_TOAST",
|
||||||
|
toast: {
|
||||||
|
...props,
|
||||||
|
id,
|
||||||
|
open: true,
|
||||||
|
onOpenChange: (open) => {
|
||||||
|
if (!open) dismiss();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
dismiss,
|
||||||
|
update,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function useToast() {
|
||||||
|
const [state, setState] = React.useState<State>(memoryState);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
listeners.push(setState);
|
||||||
|
return () => {
|
||||||
|
const index = listeners.indexOf(setState);
|
||||||
|
if (index > -1) {
|
||||||
|
listeners.splice(index, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [state]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toast,
|
||||||
|
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useToast, toast };
|
||||||
@ -41,6 +41,7 @@ import {
|
|||||||
} from "remix-themes";
|
} from "remix-themes";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { getUsageSummary } from "./services/billing.server";
|
import { getUsageSummary } from "./services/billing.server";
|
||||||
|
import { Toaster } from "./components/ui/toaster";
|
||||||
|
|
||||||
export const links: LinksFunction = () => [{ rel: "stylesheet", href: styles }];
|
export const links: LinksFunction = () => [{ rel: "stylesheet", href: styles }];
|
||||||
|
|
||||||
@ -126,6 +127,7 @@ function App() {
|
|||||||
</head>
|
</head>
|
||||||
<body className="bg-background-2 h-[100vh] h-full w-[100vw] overflow-hidden font-sans">
|
<body className="bg-background-2 h-[100vh] h-full w-[100vw] overflow-hidden font-sans">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
<Toaster />
|
||||||
<ScrollRestoration />
|
<ScrollRestoration />
|
||||||
|
|
||||||
<Scripts />
|
<Scripts />
|
||||||
|
|||||||
@ -72,27 +72,6 @@ async function createMcpServer(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle integration tools (prefixed with integration slug)
|
|
||||||
if (name.includes("_") && !name.startsWith("memory_")) {
|
|
||||||
try {
|
|
||||||
return await IntegrationLoader.callIntegrationTool(
|
|
||||||
sessionId,
|
|
||||||
name,
|
|
||||||
args,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: `Error calling integration tool: ${error instanceof Error ? error.message : String(error)}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
isError: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Unknown tool: ${name}`);
|
throw new Error(`Unknown tool: ${name}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,8 @@ const SearchParamsSchema = {
|
|||||||
properties: {
|
properties: {
|
||||||
query: {
|
query: {
|
||||||
type: "string",
|
type: "string",
|
||||||
description: "Search query as a simple statement or question. Write what you want to find, not a command. GOOD: 'user preferences for code style' or 'previous bugs in authentication' or 'GitHub integration setup'. BAD: 'search for' or 'find me' or 'get the'. Just state the topic directly.",
|
description:
|
||||||
|
"Search query as a simple statement or question. Write what you want to find, not a command. GOOD: 'user preferences for code style' or 'previous bugs in authentication' or 'GitHub integration setup'. BAD: 'search for' or 'find me' or 'get the'. Just state the topic directly.",
|
||||||
},
|
},
|
||||||
validAt: {
|
validAt: {
|
||||||
type: "string",
|
type: "string",
|
||||||
@ -36,7 +37,8 @@ const SearchParamsSchema = {
|
|||||||
items: {
|
items: {
|
||||||
type: "string",
|
type: "string",
|
||||||
},
|
},
|
||||||
description: "Optional: Array of space UUIDs to search within. Leave empty to search all spaces.",
|
description:
|
||||||
|
"Optional: Array of space UUIDs to search within. Leave empty to search all spaces.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
required: ["query"],
|
required: ["query"],
|
||||||
@ -47,14 +49,16 @@ const IngestSchema = {
|
|||||||
properties: {
|
properties: {
|
||||||
message: {
|
message: {
|
||||||
type: "string",
|
type: "string",
|
||||||
description: "The conversation text to store. Include both what the user asked and what you answered. Keep it concise but complete.",
|
description:
|
||||||
|
"The conversation text to store. Include both what the user asked and what you answered. Keep it concise but complete.",
|
||||||
},
|
},
|
||||||
spaceIds: {
|
spaceIds: {
|
||||||
type: "array",
|
type: "array",
|
||||||
items: {
|
items: {
|
||||||
type: "string",
|
type: "string",
|
||||||
},
|
},
|
||||||
description: "Optional: Array of space UUIDs (from memory_get_spaces). Add this to organize the memory by project. Example: If discussing 'core' project, include the 'core' space ID. Leave empty to store in general memory.",
|
description:
|
||||||
|
"Optional: Array of space UUIDs (from memory_get_spaces). Add this to organize the memory by project. Example: If discussing 'core' project, include the 'core' space ID. Leave empty to store in general memory.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
required: ["message"],
|
required: ["message"],
|
||||||
@ -82,7 +86,8 @@ export const memoryTools = [
|
|||||||
properties: {
|
properties: {
|
||||||
all: {
|
all: {
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
description: "Set to true to get all spaces including system spaces. Leave empty for user spaces only.",
|
description:
|
||||||
|
"Set to true to get all spaces including system spaces. Leave empty for user spaces only.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -96,7 +101,8 @@ export const memoryTools = [
|
|||||||
properties: {
|
properties: {
|
||||||
profile: {
|
profile: {
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
description: "Set to true to get full profile. Leave empty for default profile view.",
|
description:
|
||||||
|
"Set to true to get full profile. Leave empty for default profile view.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -110,11 +116,13 @@ export const memoryTools = [
|
|||||||
properties: {
|
properties: {
|
||||||
spaceId: {
|
spaceId: {
|
||||||
type: "string",
|
type: "string",
|
||||||
description: "UUID of the space (use this if you have the ID from memory_get_spaces)",
|
description:
|
||||||
|
"UUID of the space (use this if you have the ID from memory_get_spaces)",
|
||||||
},
|
},
|
||||||
spaceName: {
|
spaceName: {
|
||||||
type: "string",
|
type: "string",
|
||||||
description: "Name of the space (easier option). Examples: 'core', 'Profile', 'GitHub', 'Health'",
|
description:
|
||||||
|
"Name of the space (easier option). Examples: 'core', 'Profile', 'GitHub', 'Health'",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -137,7 +145,8 @@ export const memoryTools = [
|
|||||||
properties: {
|
properties: {
|
||||||
integrationSlug: {
|
integrationSlug: {
|
||||||
type: "string",
|
type: "string",
|
||||||
description: "Slug from get_integrations. Examples: 'github', 'linear', 'slack'",
|
description:
|
||||||
|
"Slug from get_integrations. Examples: 'github', 'linear', 'slack'",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
required: ["integrationSlug"],
|
required: ["integrationSlug"],
|
||||||
@ -152,15 +161,18 @@ export const memoryTools = [
|
|||||||
properties: {
|
properties: {
|
||||||
integrationSlug: {
|
integrationSlug: {
|
||||||
type: "string",
|
type: "string",
|
||||||
description: "Slug from get_integrations. Examples: 'github', 'linear', 'slack'",
|
description:
|
||||||
|
"Slug from get_integrations. Examples: 'github', 'linear', 'slack'",
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: "string",
|
type: "string",
|
||||||
description: "Action name from get_integration_actions. Examples: 'get_pr', 'get_issues', 'create_issue'",
|
description:
|
||||||
|
"Action name from get_integration_actions. Examples: 'get_pr', 'get_issues', 'create_issue'",
|
||||||
},
|
},
|
||||||
arguments: {
|
arguments: {
|
||||||
type: "object",
|
type: "object",
|
||||||
description: "Parameters for the action. Check the action's inputSchema from get_integration_actions to see what's required.",
|
description:
|
||||||
|
"Parameters for the action. Check the action's inputSchema from get_integration_actions to see what's required.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
required: ["integrationSlug", "action"],
|
required: ["integrationSlug", "action"],
|
||||||
@ -242,7 +254,8 @@ async function handleUserProfile(userId: string) {
|
|||||||
async function handleMemoryIngest(args: any) {
|
async function handleMemoryIngest(args: any) {
|
||||||
try {
|
try {
|
||||||
// Use spaceIds from args if provided, otherwise use spaceId from query params
|
// Use spaceIds from args if provided, otherwise use spaceId from query params
|
||||||
const spaceIds = args.spaceIds || (args.spaceId ? [args.spaceId] : undefined);
|
const spaceIds =
|
||||||
|
args.spaceIds || (args.spaceId ? [args.spaceId] : undefined);
|
||||||
|
|
||||||
const response = await addToQueue(
|
const response = await addToQueue(
|
||||||
{
|
{
|
||||||
@ -284,7 +297,8 @@ async function handleMemoryIngest(args: any) {
|
|||||||
async function handleMemorySearch(args: any) {
|
async function handleMemorySearch(args: any) {
|
||||||
try {
|
try {
|
||||||
// Use spaceIds from args if provided, otherwise use spaceId from query params
|
// Use spaceIds from args if provided, otherwise use spaceId from query params
|
||||||
const spaceIds = args.spaceIds || (args.spaceId ? [args.spaceId] : undefined);
|
const spaceIds =
|
||||||
|
args.spaceIds || (args.spaceId ? [args.spaceId] : undefined);
|
||||||
|
|
||||||
const results = await searchService.search(
|
const results = await searchService.search(
|
||||||
args.query,
|
args.query,
|
||||||
@ -328,7 +342,6 @@ async function handleMemoryGetSpaces(userId: string) {
|
|||||||
const simplifiedSpaces = spaces.map((space) => ({
|
const simplifiedSpaces = spaces.map((space) => ({
|
||||||
id: space.id,
|
id: space.id,
|
||||||
name: space.name,
|
name: space.name,
|
||||||
description: space.description,
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -379,7 +392,6 @@ async function handleGetSpace(args: any) {
|
|||||||
const spaceDetails = {
|
const spaceDetails = {
|
||||||
id: space.id,
|
id: space.id,
|
||||||
name: space.name,
|
name: space.name,
|
||||||
description: space.description,
|
|
||||||
summary: space.summary,
|
summary: space.summary,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -426,7 +438,7 @@ async function handleGetIntegrations(args: any) {
|
|||||||
slug: account.integrationDefinition.slug,
|
slug: account.integrationDefinition.slug,
|
||||||
name: account.integrationDefinition.name,
|
name: account.integrationDefinition.name,
|
||||||
accountId: account.id,
|
accountId: account.id,
|
||||||
hasMcp: !!(account.integrationDefinition.spec?.mcp),
|
hasMcp: !!account.integrationDefinition.spec?.mcp,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user