mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 00:36:31 +02:00
chore: format upstream files
This commit is contained in:
parent
a73bab89d9
commit
1fd3a36c57
17 changed files with 483 additions and 222 deletions
|
|
@ -744,7 +744,11 @@ export function DocumentsTableShell({
|
|||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
<DropdownMenuItem onClick={() => onOpenInTab ? onOpenInTab(doc) : handleViewDocument(doc)}>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
onOpenInTab ? onOpenInTab(doc) : handleViewDocument(doc)
|
||||
}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
Open
|
||||
</DropdownMenuItem>
|
||||
|
|
|
|||
|
|
@ -1581,4 +1581,4 @@ export default function NewChatPage() {
|
|||
</div>
|
||||
</AssistantRuntimeProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -308,7 +308,8 @@ export function TeamContent({ searchSpaceId }: TeamContentProps) {
|
|||
{invitesLoading ? (
|
||||
<Skeleton className="h-9 w-32 rounded-md" />
|
||||
) : (
|
||||
canInvite && activeInvites.length > 0 && (
|
||||
canInvite &&
|
||||
activeInvites.length > 0 && (
|
||||
<AllInvitesDialog invites={activeInvites} onRevokeInvite={handleRevokeInvite} />
|
||||
)
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,14 @@ import {
|
|||
useAuiState,
|
||||
} from "@assistant-ui/react";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { CheckIcon, ClipboardPaste, CopyIcon, DownloadIcon, MessageSquare, RefreshCwIcon } from "lucide-react";
|
||||
import {
|
||||
CheckIcon,
|
||||
ClipboardPaste,
|
||||
CopyIcon,
|
||||
DownloadIcon,
|
||||
MessageSquare,
|
||||
RefreshCwIcon,
|
||||
} from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { commentsEnabledAtom, targetCommentIdAtom } from "@/atoms/chat/current-thread.atom";
|
||||
|
|
@ -40,10 +47,6 @@ import {
|
|||
CreateGoogleDriveFileToolUI,
|
||||
DeleteGoogleDriveFileToolUI,
|
||||
} from "@/components/tool-ui/google-drive";
|
||||
import {
|
||||
CreateOneDriveFileToolUI,
|
||||
DeleteOneDriveFileToolUI,
|
||||
} from "@/components/tool-ui/onedrive";
|
||||
import {
|
||||
CreateJiraIssueToolUI,
|
||||
DeleteJiraIssueToolUI,
|
||||
|
|
@ -59,6 +62,7 @@ import {
|
|||
DeleteNotionPageToolUI,
|
||||
UpdateNotionPageToolUI,
|
||||
} from "@/components/tool-ui/notion";
|
||||
import { CreateOneDriveFileToolUI, DeleteOneDriveFileToolUI } from "@/components/tool-ui/onedrive";
|
||||
import { SandboxExecuteToolUI } from "@/components/tool-ui/sandbox-execute";
|
||||
import { RecallMemoryToolUI, SaveMemoryToolUI } from "@/components/tool-ui/user-memory";
|
||||
import { GenerateVideoPresentationToolUI } from "@/components/tool-ui/video-presentation";
|
||||
|
|
|
|||
|
|
@ -340,11 +340,11 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
|
|||
onBack={handleBackFromEdit}
|
||||
onQuickIndex={(() => {
|
||||
const cfg = connectorConfig || editingConnector.config;
|
||||
const isDriveOrOneDrive =
|
||||
editingConnector.connector_type === "GOOGLE_DRIVE_CONNECTOR" ||
|
||||
editingConnector.connector_type === "COMPOSIO_GOOGLE_DRIVE_CONNECTOR" ||
|
||||
editingConnector.connector_type === "ONEDRIVE_CONNECTOR";
|
||||
const hasDriveItems = isDriveOrOneDrive
|
||||
const isDriveOrOneDrive =
|
||||
editingConnector.connector_type === "GOOGLE_DRIVE_CONNECTOR" ||
|
||||
editingConnector.connector_type === "COMPOSIO_GOOGLE_DRIVE_CONNECTOR" ||
|
||||
editingConnector.connector_type === "ONEDRIVE_CONNECTOR";
|
||||
const hasDriveItems = isDriveOrOneDrive
|
||||
? ((cfg?.selected_folders as unknown[]) ?? []).length > 0 ||
|
||||
((cfg?.selected_files as unknown[]) ?? []).length > 0
|
||||
: true;
|
||||
|
|
|
|||
|
|
@ -212,8 +212,7 @@ export const OneDriveConfig: FC<ConnectorConfigProps> = ({ connector, onConfigCh
|
|||
|
||||
{isAuthExpired && (
|
||||
<p className="text-xs text-amber-600 dark:text-amber-500">
|
||||
Your OneDrive authentication has expired. Please re-authenticate using the button
|
||||
below.
|
||||
Your OneDrive authentication has expired. Please re-authenticate using the button below.
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@ import { LinkupApiConfig } from "./components/linkup-api-config";
|
|||
import { LumaConfig } from "./components/luma-config";
|
||||
import { MCPConfig } from "./components/mcp-config";
|
||||
import { ObsidianConfig } from "./components/obsidian-config";
|
||||
import { OneDriveConfig } from "./components/onedrive-config";
|
||||
import { SlackConfig } from "./components/slack-config";
|
||||
import { TavilyApiConfig } from "./components/tavily-api-config";
|
||||
import { OneDriveConfig } from "./components/onedrive-config";
|
||||
import { TeamsConfig } from "./components/teams-config";
|
||||
import { WebcrawlerConfig } from "./components/webcrawler-config";
|
||||
|
||||
|
|
|
|||
|
|
@ -779,11 +779,11 @@ export const useConnectorDialog = () => {
|
|||
});
|
||||
}
|
||||
|
||||
// Handle Google Drive / OneDrive folder selection (regular and Composio)
|
||||
if (
|
||||
(indexingConfig.connectorType === "GOOGLE_DRIVE_CONNECTOR" ||
|
||||
indexingConfig.connectorType === "COMPOSIO_GOOGLE_DRIVE_CONNECTOR" ||
|
||||
indexingConfig.connectorType === "ONEDRIVE_CONNECTOR") &&
|
||||
// Handle Google Drive / OneDrive folder selection (regular and Composio)
|
||||
if (
|
||||
(indexingConfig.connectorType === "GOOGLE_DRIVE_CONNECTOR" ||
|
||||
indexingConfig.connectorType === "COMPOSIO_GOOGLE_DRIVE_CONNECTOR" ||
|
||||
indexingConfig.connectorType === "ONEDRIVE_CONNECTOR") &&
|
||||
indexingConnectorConfig
|
||||
) {
|
||||
const selectedFolders = indexingConnectorConfig.selected_folders as
|
||||
|
|
|
|||
|
|
@ -544,7 +544,12 @@ export const InlineMentionEditor = forwardRef<InlineMentionEditorRef, InlineMent
|
|||
}
|
||||
}
|
||||
|
||||
if (slashIndex !== -1 && (slashIndex === 0 || textContent[slashIndex - 1] === " " || textContent[slashIndex - 1] === "\n")) {
|
||||
if (
|
||||
slashIndex !== -1 &&
|
||||
(slashIndex === 0 ||
|
||||
textContent[slashIndex - 1] === " " ||
|
||||
textContent[slashIndex - 1] === "\n")
|
||||
) {
|
||||
const query = textContent.slice(slashIndex + 1, cursorPos);
|
||||
if (!query.startsWith(" ")) {
|
||||
shouldTriggerAction = true;
|
||||
|
|
@ -575,7 +580,15 @@ export const InlineMentionEditor = forwardRef<InlineMentionEditorRef, InlineMent
|
|||
|
||||
// Notify parent of change
|
||||
onChange?.(text, Array.from(mentionedDocs.values()));
|
||||
}, [getText, mentionedDocs, onChange, onMentionTrigger, onMentionClose, onActionTrigger, onActionClose]);
|
||||
}, [
|
||||
getText,
|
||||
mentionedDocs,
|
||||
onChange,
|
||||
onMentionTrigger,
|
||||
onMentionClose,
|
||||
onActionTrigger,
|
||||
onActionClose,
|
||||
]);
|
||||
|
||||
// Handle keydown
|
||||
const handleKeyDown = useCallback(
|
||||
|
|
|
|||
|
|
@ -394,7 +394,10 @@ const defaultComponents = memoizeMarkdownComponents({
|
|||
if (!isCodeBlock) {
|
||||
return (
|
||||
<code
|
||||
className={cn("aui-md-inline-code rounded-md border bg-muted px-1.5 py-0.5 font-mono text-[0.9em] font-normal", className)}
|
||||
className={cn(
|
||||
"aui-md-inline-code rounded-md border bg-muted px-1.5 py-0.5 font-mono text-[0.9em] font-normal",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -60,11 +60,11 @@ import {
|
|||
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
||||
import { UserMessage } from "@/components/assistant-ui/user-message";
|
||||
import { SLIDEOUT_PANEL_OPENED_EVENT } from "@/components/layout/ui/sidebar/SidebarSlideOutPanel";
|
||||
import { PromptPicker, type PromptPickerRef } from "@/components/new-chat/prompt-picker";
|
||||
import {
|
||||
DocumentMentionPicker,
|
||||
type DocumentMentionPickerRef,
|
||||
} from "@/components/new-chat/document-mention-picker";
|
||||
import { PromptPicker, type PromptPickerRef } from "@/components/new-chat/prompt-picker";
|
||||
import { Avatar, AvatarFallback, AvatarGroup } from "@/components/ui/avatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Drawer, DrawerContent, DrawerHandle, DrawerTitle } from "@/components/ui/drawer";
|
||||
|
|
@ -490,7 +490,9 @@ const Composer: FC = () => {
|
|||
}
|
||||
const finalPrompt = action.prompt.includes("{selection}")
|
||||
? action.prompt.replace("{selection}", () => userText)
|
||||
: userText ? `${action.prompt}\n\n${userText}` : action.prompt;
|
||||
: userText
|
||||
? `${action.prompt}\n\n${userText}`
|
||||
: action.prompt;
|
||||
aui.composer().setText(finalPrompt);
|
||||
aui.composer().send();
|
||||
editorRef.current?.clear();
|
||||
|
|
@ -582,9 +584,7 @@ const Composer: FC = () => {
|
|||
if (!showDocumentPopover && !showPromptPicker) {
|
||||
if (clipboardInitialText) {
|
||||
const userText = editorRef.current?.getText() ?? "";
|
||||
const combined = userText
|
||||
? `${userText}\n\n${clipboardInitialText}`
|
||||
: clipboardInitialText;
|
||||
const combined = userText ? `${userText}\n\n${clipboardInitialText}` : clipboardInitialText;
|
||||
aui.composer().setText(combined);
|
||||
setClipboardInitialText(undefined);
|
||||
}
|
||||
|
|
@ -640,7 +640,7 @@ const Composer: FC = () => {
|
|||
return (
|
||||
<ComposerPrimitive.Root
|
||||
className="aui-composer-root relative flex w-full flex-col gap-2"
|
||||
style={(showPromptPicker && clipboardInitialText) ? { marginBottom: 220 } : undefined}
|
||||
style={showPromptPicker && clipboardInitialText ? { marginBottom: 220 } : undefined}
|
||||
>
|
||||
<ChatSessionStatus
|
||||
isAiResponding={isAiResponding}
|
||||
|
|
@ -648,7 +648,10 @@ const Composer: FC = () => {
|
|||
currentUserId={currentUser?.id ?? null}
|
||||
members={members ?? []}
|
||||
/>
|
||||
<div ref={composerBoxRef} className="aui-composer-attachment-dropzone flex w-full flex-col overflow-hidden rounded-2xl border-input bg-muted pt-2 outline-none transition-shadow">
|
||||
<div
|
||||
ref={composerBoxRef}
|
||||
className="aui-composer-attachment-dropzone flex w-full flex-col overflow-hidden rounded-2xl border-input bg-muted pt-2 outline-none transition-shadow"
|
||||
>
|
||||
{clipboardInitialText && (
|
||||
<ClipboardChip
|
||||
text={clipboardInitialText}
|
||||
|
|
@ -711,10 +714,11 @@ const Composer: FC = () => {
|
|||
position: "fixed",
|
||||
...(clipboardInitialText && composerBoxRef.current
|
||||
? { top: `${composerBoxRef.current.getBoundingClientRect().bottom + 8}px` }
|
||||
: { bottom: editorContainerRef.current
|
||||
? `${window.innerHeight - editorContainerRef.current.getBoundingClientRect().top + 8}px`
|
||||
: "200px" }
|
||||
),
|
||||
: {
|
||||
bottom: editorContainerRef.current
|
||||
? `${window.innerHeight - editorContainerRef.current.getBoundingClientRect().top + 8}px`
|
||||
: "200px",
|
||||
}),
|
||||
left: editorContainerRef.current
|
||||
? `${editorContainerRef.current.getBoundingClientRect().left}px`
|
||||
: "50%",
|
||||
|
|
|
|||
|
|
@ -240,7 +240,9 @@ export function FolderTreeView({
|
|||
return (
|
||||
<div className="flex flex-1 flex-col items-center justify-center gap-1 px-4 py-12 text-muted-foreground">
|
||||
<p className="text-sm font-medium">No documents found</p>
|
||||
<p className="text-xs text-muted-foreground/70">Use the upload button or connect a source above</p>
|
||||
<p className="text-xs text-muted-foreground/70">
|
||||
Use the upload button or connect a source above
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,21 +7,21 @@ import { useParams } from "next/navigation";
|
|||
import { useTranslations } from "next-intl";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { MarkdownViewer } from "@/components/markdown-viewer";
|
||||
import { DocumentsFilters } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsFilters";
|
||||
import { sidebarSelectedDocumentsAtom } from "@/atoms/chat/mentioned-documents.atom";
|
||||
import { connectorDialogOpenAtom } from "@/atoms/connector-dialog/connector-dialog.atoms";
|
||||
import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms";
|
||||
import { deleteDocumentMutationAtom } from "@/atoms/documents/document-mutation.atoms";
|
||||
import { expandedFolderIdsAtom } from "@/atoms/documents/folder.atoms";
|
||||
import { rightPanelCollapsedAtom } from "@/atoms/layout/right-panel.atom";
|
||||
import { agentCreatedDocumentsAtom } from "@/atoms/documents/ui.atoms";
|
||||
import { rightPanelCollapsedAtom } from "@/atoms/layout/right-panel.atom";
|
||||
import { openDocumentTabAtom } from "@/atoms/tabs/tabs.atom";
|
||||
import { CreateFolderDialog } from "@/components/documents/CreateFolderDialog";
|
||||
import type { DocumentNodeDoc } from "@/components/documents/DocumentNode";
|
||||
import type { FolderDisplay } from "@/components/documents/FolderNode";
|
||||
import { FolderPickerDialog } from "@/components/documents/FolderPickerDialog";
|
||||
import { FolderTreeView } from "@/components/documents/FolderTreeView";
|
||||
import { MarkdownViewer } from "@/components/markdown-viewer";
|
||||
import { EXPORT_FILE_EXTENSIONS } from "@/components/shared/ExportMenuItems";
|
||||
import {
|
||||
AlertDialog,
|
||||
|
|
@ -49,8 +49,8 @@ import type { DocumentTypeEnum } from "@/contracts/types/document.types";
|
|||
import { useDebouncedValue } from "@/hooks/use-debounced-value";
|
||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { foldersApiService } from "@/lib/apis/folders-api.service";
|
||||
import { documentsApiService } from "@/lib/apis/documents-api.service";
|
||||
import { foldersApiService } from "@/lib/apis/folders-api.service";
|
||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||
import { queries } from "@/zero/queries/index";
|
||||
import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useSetAtom } from "jotai";
|
||||
import {
|
||||
BookOpen,
|
||||
Check,
|
||||
|
|
@ -8,11 +9,10 @@ import {
|
|||
List,
|
||||
Minimize2,
|
||||
PenLine,
|
||||
Plus,
|
||||
Search,
|
||||
Zap,
|
||||
Plus,
|
||||
} from "lucide-react";
|
||||
import { useSetAtom } from "jotai";
|
||||
import {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
|
|
@ -53,113 +53,192 @@ const ICONS: Record<string, React.ReactNode> = {
|
|||
zap: <Zap className="size-3.5" />,
|
||||
};
|
||||
|
||||
const DEFAULT_ACTIONS: { name: string; prompt: string; mode: "transform" | "explore"; icon: string }[] = [
|
||||
{ name: "Fix grammar", prompt: "Fix the grammar and spelling in the following text. Return only the corrected text, nothing else.\n\n{selection}", mode: "transform", icon: "check" },
|
||||
{ name: "Make shorter", prompt: "Make the following text more concise while preserving its meaning. Return only the shortened text, nothing else.\n\n{selection}", mode: "transform", icon: "minimize" },
|
||||
{ name: "Translate", prompt: "Translate the following text to English. If it is already in English, translate it to French. Return only the translation, nothing else.\n\n{selection}", mode: "transform", icon: "languages" },
|
||||
{ name: "Rewrite", prompt: "Rewrite the following text to improve clarity and readability. Return only the rewritten text, nothing else.\n\n{selection}", mode: "transform", icon: "pen-line" },
|
||||
{ name: "Summarize", prompt: "Summarize the following text concisely. Return only the summary, nothing else.\n\n{selection}", mode: "transform", icon: "list" },
|
||||
{ name: "Explain", prompt: "Explain the following text in simple terms:\n\n{selection}", mode: "explore", icon: "book-open" },
|
||||
{ name: "Ask my knowledge base", prompt: "Search my knowledge base for information related to:\n\n{selection}", mode: "explore", icon: "search" },
|
||||
{ name: "Look up on the web", prompt: "Search the web for information about:\n\n{selection}", mode: "explore", icon: "globe" },
|
||||
const DEFAULT_ACTIONS: {
|
||||
name: string;
|
||||
prompt: string;
|
||||
mode: "transform" | "explore";
|
||||
icon: string;
|
||||
}[] = [
|
||||
{
|
||||
name: "Fix grammar",
|
||||
prompt:
|
||||
"Fix the grammar and spelling in the following text. Return only the corrected text, nothing else.\n\n{selection}",
|
||||
mode: "transform",
|
||||
icon: "check",
|
||||
},
|
||||
{
|
||||
name: "Make shorter",
|
||||
prompt:
|
||||
"Make the following text more concise while preserving its meaning. Return only the shortened text, nothing else.\n\n{selection}",
|
||||
mode: "transform",
|
||||
icon: "minimize",
|
||||
},
|
||||
{
|
||||
name: "Translate",
|
||||
prompt:
|
||||
"Translate the following text to English. If it is already in English, translate it to French. Return only the translation, nothing else.\n\n{selection}",
|
||||
mode: "transform",
|
||||
icon: "languages",
|
||||
},
|
||||
{
|
||||
name: "Rewrite",
|
||||
prompt:
|
||||
"Rewrite the following text to improve clarity and readability. Return only the rewritten text, nothing else.\n\n{selection}",
|
||||
mode: "transform",
|
||||
icon: "pen-line",
|
||||
},
|
||||
{
|
||||
name: "Summarize",
|
||||
prompt:
|
||||
"Summarize the following text concisely. Return only the summary, nothing else.\n\n{selection}",
|
||||
mode: "transform",
|
||||
icon: "list",
|
||||
},
|
||||
{
|
||||
name: "Explain",
|
||||
prompt: "Explain the following text in simple terms:\n\n{selection}",
|
||||
mode: "explore",
|
||||
icon: "book-open",
|
||||
},
|
||||
{
|
||||
name: "Ask my knowledge base",
|
||||
prompt: "Search my knowledge base for information related to:\n\n{selection}",
|
||||
mode: "explore",
|
||||
icon: "search",
|
||||
},
|
||||
{
|
||||
name: "Look up on the web",
|
||||
prompt: "Search the web for information about:\n\n{selection}",
|
||||
mode: "explore",
|
||||
icon: "globe",
|
||||
},
|
||||
];
|
||||
|
||||
export const PromptPicker = forwardRef<PromptPickerRef, PromptPickerProps>(
|
||||
function PromptPicker({ onSelect, onDone, externalSearch = "", containerStyle }, ref) {
|
||||
const setUserSettingsDialog = useSetAtom(userSettingsDialogAtom);
|
||||
const [highlightedIndex, setHighlightedIndex] = useState(0);
|
||||
const [customPrompts, setCustomPrompts] = useState<PromptRead[]>([]);
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const shouldScrollRef = useRef(false);
|
||||
const itemRefs = useRef<Map<number, HTMLButtonElement>>(new Map());
|
||||
export const PromptPicker = forwardRef<PromptPickerRef, PromptPickerProps>(function PromptPicker(
|
||||
{ onSelect, onDone, externalSearch = "", containerStyle },
|
||||
ref
|
||||
) {
|
||||
const setUserSettingsDialog = useSetAtom(userSettingsDialogAtom);
|
||||
const [highlightedIndex, setHighlightedIndex] = useState(0);
|
||||
const [customPrompts, setCustomPrompts] = useState<PromptRead[]>([]);
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const shouldScrollRef = useRef(false);
|
||||
const itemRefs = useRef<Map<number, HTMLButtonElement>>(new Map());
|
||||
|
||||
useEffect(() => {
|
||||
promptsApiService.list().then(setCustomPrompts).catch(() => {});
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
promptsApiService
|
||||
.list()
|
||||
.then(setCustomPrompts)
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const allActions = useMemo(() => {
|
||||
const customs = customPrompts.map((a) => ({
|
||||
name: a.name,
|
||||
prompt: a.prompt,
|
||||
mode: a.mode as "transform" | "explore",
|
||||
icon: a.icon || "zap",
|
||||
}));
|
||||
return [...DEFAULT_ACTIONS, ...customs];
|
||||
}, [customPrompts]);
|
||||
const allActions = useMemo(() => {
|
||||
const customs = customPrompts.map((a) => ({
|
||||
name: a.name,
|
||||
prompt: a.prompt,
|
||||
mode: a.mode as "transform" | "explore",
|
||||
icon: a.icon || "zap",
|
||||
}));
|
||||
return [...DEFAULT_ACTIONS, ...customs];
|
||||
}, [customPrompts]);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
if (!externalSearch) return allActions;
|
||||
return allActions.filter((a) =>
|
||||
a.name.toLowerCase().includes(externalSearch.toLowerCase())
|
||||
);
|
||||
}, [allActions, externalSearch]);
|
||||
const filtered = useMemo(() => {
|
||||
if (!externalSearch) return allActions;
|
||||
return allActions.filter((a) => a.name.toLowerCase().includes(externalSearch.toLowerCase()));
|
||||
}, [allActions, externalSearch]);
|
||||
|
||||
// Reset highlight when results change
|
||||
const prevSearchRef = useRef(externalSearch);
|
||||
if (prevSearchRef.current !== externalSearch) {
|
||||
prevSearchRef.current = externalSearch;
|
||||
if (highlightedIndex !== 0) {
|
||||
setHighlightedIndex(0);
|
||||
}
|
||||
// Reset highlight when results change
|
||||
const prevSearchRef = useRef(externalSearch);
|
||||
if (prevSearchRef.current !== externalSearch) {
|
||||
prevSearchRef.current = externalSearch;
|
||||
if (highlightedIndex !== 0) {
|
||||
setHighlightedIndex(0);
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(index: number) => {
|
||||
const action = filtered[index];
|
||||
if (!action) return;
|
||||
onSelect({ name: action.name, prompt: action.prompt, mode: action.mode });
|
||||
},
|
||||
[filtered, onSelect]
|
||||
);
|
||||
const handleSelect = useCallback(
|
||||
(index: number) => {
|
||||
const action = filtered[index];
|
||||
if (!action) return;
|
||||
onSelect({ name: action.name, prompt: action.prompt, mode: action.mode });
|
||||
},
|
||||
[filtered, onSelect]
|
||||
);
|
||||
|
||||
// Auto-scroll highlighted item into view
|
||||
useEffect(() => {
|
||||
if (!shouldScrollRef.current) return;
|
||||
shouldScrollRef.current = false;
|
||||
// Auto-scroll highlighted item into view
|
||||
useEffect(() => {
|
||||
if (!shouldScrollRef.current) return;
|
||||
shouldScrollRef.current = false;
|
||||
|
||||
const rafId = requestAnimationFrame(() => {
|
||||
const item = itemRefs.current.get(highlightedIndex);
|
||||
const container = scrollContainerRef.current;
|
||||
if (item && container) {
|
||||
const itemRect = item.getBoundingClientRect();
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
if (itemRect.top < containerRect.top || itemRect.bottom > containerRect.bottom) {
|
||||
item.scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
const rafId = requestAnimationFrame(() => {
|
||||
const item = itemRefs.current.get(highlightedIndex);
|
||||
const container = scrollContainerRef.current;
|
||||
if (item && container) {
|
||||
const itemRect = item.getBoundingClientRect();
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
if (itemRect.top < containerRect.top || itemRect.bottom > containerRect.bottom) {
|
||||
item.scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return () => cancelAnimationFrame(rafId);
|
||||
}, [highlightedIndex]);
|
||||
return () => cancelAnimationFrame(rafId);
|
||||
}, [highlightedIndex]);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
selectHighlighted: () => handleSelect(highlightedIndex),
|
||||
moveUp: () => {
|
||||
shouldScrollRef.current = true;
|
||||
setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : filtered.length - 1));
|
||||
},
|
||||
moveDown: () => {
|
||||
shouldScrollRef.current = true;
|
||||
setHighlightedIndex((prev) => (prev < filtered.length - 1 ? prev + 1 : 0));
|
||||
},
|
||||
}),
|
||||
[filtered.length, highlightedIndex, handleSelect]
|
||||
);
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
selectHighlighted: () => handleSelect(highlightedIndex),
|
||||
moveUp: () => {
|
||||
shouldScrollRef.current = true;
|
||||
setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : filtered.length - 1));
|
||||
},
|
||||
moveDown: () => {
|
||||
shouldScrollRef.current = true;
|
||||
setHighlightedIndex((prev) => (prev < filtered.length - 1 ? prev + 1 : 0));
|
||||
},
|
||||
}),
|
||||
[filtered.length, highlightedIndex, handleSelect]
|
||||
);
|
||||
|
||||
if (filtered.length === 0) return null;
|
||||
if (filtered.length === 0) return null;
|
||||
|
||||
const defaultFiltered = filtered.filter((_, i) => i < DEFAULT_ACTIONS.length);
|
||||
const customFiltered = filtered.filter((_, i) => i >= DEFAULT_ACTIONS.length);
|
||||
const defaultFiltered = filtered.filter((_, i) => i < DEFAULT_ACTIONS.length);
|
||||
const customFiltered = filtered.filter((_, i) => i >= DEFAULT_ACTIONS.length);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-64 rounded-lg border bg-popover shadow-lg overflow-hidden"
|
||||
style={containerStyle}
|
||||
>
|
||||
<div ref={scrollContainerRef} className="max-h-48 overflow-y-auto py-1">
|
||||
{defaultFiltered.map((action, index) => (
|
||||
return (
|
||||
<div
|
||||
className="w-64 rounded-lg border bg-popover shadow-lg overflow-hidden"
|
||||
style={containerStyle}
|
||||
>
|
||||
<div ref={scrollContainerRef} className="max-h-48 overflow-y-auto py-1">
|
||||
{defaultFiltered.map((action, index) => (
|
||||
<button
|
||||
key={action.name}
|
||||
ref={(el) => {
|
||||
if (el) itemRefs.current.set(index, el);
|
||||
else itemRefs.current.delete(index);
|
||||
}}
|
||||
type="button"
|
||||
onClick={() => handleSelect(index)}
|
||||
onMouseEnter={() => setHighlightedIndex(index)}
|
||||
className={cn(
|
||||
"flex w-full items-center gap-2 px-3 py-1.5 text-sm cursor-pointer",
|
||||
index === highlightedIndex ? "bg-accent" : "hover:bg-accent/50"
|
||||
)}
|
||||
>
|
||||
<span className="text-muted-foreground">
|
||||
{ICONS[action.icon] ?? <Zap className="size-3.5" />}
|
||||
</span>
|
||||
<span className="truncate">{action.name}</span>
|
||||
</button>
|
||||
))}
|
||||
|
||||
{customFiltered.length > 0 && <div className="my-1 h-px bg-border mx-2" />}
|
||||
|
||||
{customFiltered.map((action, i) => {
|
||||
const index = defaultFiltered.length + i;
|
||||
return (
|
||||
<button
|
||||
key={action.name}
|
||||
ref={(el) => {
|
||||
|
|
@ -174,52 +253,27 @@ export const PromptPicker = forwardRef<PromptPickerRef, PromptPickerProps>(
|
|||
index === highlightedIndex ? "bg-accent" : "hover:bg-accent/50"
|
||||
)}
|
||||
>
|
||||
<span className="text-muted-foreground">{ICONS[action.icon] ?? <Zap className="size-3.5" />}</span>
|
||||
<span className="text-muted-foreground">
|
||||
<Zap className="size-3.5" />
|
||||
</span>
|
||||
<span className="truncate">{action.name}</span>
|
||||
</button>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
|
||||
{customFiltered.length > 0 && (
|
||||
<div className="my-1 h-px bg-border mx-2" />
|
||||
)}
|
||||
|
||||
{customFiltered.map((action, i) => {
|
||||
const index = defaultFiltered.length + i;
|
||||
return (
|
||||
<button
|
||||
key={action.name}
|
||||
ref={(el) => {
|
||||
if (el) itemRefs.current.set(index, el);
|
||||
else itemRefs.current.delete(index);
|
||||
}}
|
||||
type="button"
|
||||
onClick={() => handleSelect(index)}
|
||||
onMouseEnter={() => setHighlightedIndex(index)}
|
||||
className={cn(
|
||||
"flex w-full items-center gap-2 px-3 py-1.5 text-sm cursor-pointer",
|
||||
index === highlightedIndex ? "bg-accent" : "hover:bg-accent/50"
|
||||
)}
|
||||
>
|
||||
<span className="text-muted-foreground"><Zap className="size-3.5" /></span>
|
||||
<span className="truncate">{action.name}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="my-1 h-px bg-border mx-2" />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onDone();
|
||||
setUserSettingsDialog({ open: true, initialTab: "prompts" });
|
||||
}}
|
||||
className="flex w-full items-center gap-2 px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground hover:bg-accent/50 cursor-pointer"
|
||||
>
|
||||
<Plus className="size-3.5" />
|
||||
<span>Create prompt</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="my-1 h-px bg-border mx-2" />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onDone();
|
||||
setUserSettingsDialog({ open: true, initialTab: "prompts" });
|
||||
}}
|
||||
className="flex w-full items-center gap-2 px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground hover:bg-accent/50 cursor-pointer"
|
||||
>
|
||||
<Plus className="size-3.5" />
|
||||
<span>Create prompt</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ export {
|
|||
export { GeneratePodcastToolUI } from "./generate-podcast";
|
||||
export { GenerateReportToolUI } from "./generate-report";
|
||||
export { CreateGoogleDriveFileToolUI, DeleteGoogleDriveFileToolUI } from "./google-drive";
|
||||
export { CreateOneDriveFileToolUI, DeleteOneDriveFileToolUI } from "./onedrive";
|
||||
export {
|
||||
Image,
|
||||
ImageErrorBoundary,
|
||||
|
|
@ -33,6 +32,7 @@ export {
|
|||
UpdateLinearIssueToolUI,
|
||||
} from "./linear";
|
||||
export { CreateNotionPageToolUI, DeleteNotionPageToolUI, UpdateNotionPageToolUI } from "./notion";
|
||||
export { CreateOneDriveFileToolUI, DeleteOneDriveFileToolUI } from "./onedrive";
|
||||
export {
|
||||
Plan,
|
||||
PlanErrorBoundary,
|
||||
|
|
|
|||
|
|
@ -270,9 +270,7 @@ function ApprovalCard({
|
|||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-medium text-muted-foreground">
|
||||
File Type
|
||||
</p>
|
||||
<p className="text-xs font-medium text-muted-foreground">File Type</p>
|
||||
<Select value="docx" disabled>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue />
|
||||
|
|
@ -315,11 +313,25 @@ function ApprovalCard({
|
|||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 pt-3">
|
||||
{(pendingEdits?.name ?? args.name) != null && (
|
||||
<p className="text-sm font-medium text-foreground">{String(pendingEdits?.name ?? args.name)}</p>
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
{String(pendingEdits?.name ?? args.name)}
|
||||
</p>
|
||||
)}
|
||||
{(pendingEdits?.content ?? args.content) != null && (
|
||||
<div className="mt-2 max-h-[7rem] overflow-hidden text-sm" style={{ maskImage: "linear-gradient(to bottom, black 50%, transparent 100%)", WebkitMaskImage: "linear-gradient(to bottom, black 50%, transparent 100%)" }}>
|
||||
<PlateEditor markdown={String(pendingEdits?.content ?? args.content)} readOnly preset="readonly" editorVariant="none" className="h-auto [&_[data-slate-editor]]:!min-h-0 [&_[data-slate-editor]>*:first-child]:!mt-0" />
|
||||
<div
|
||||
className="mt-2 max-h-[7rem] overflow-hidden text-sm"
|
||||
style={{
|
||||
maskImage: "linear-gradient(to bottom, black 50%, transparent 100%)",
|
||||
WebkitMaskImage: "linear-gradient(to bottom, black 50%, transparent 100%)",
|
||||
}}
|
||||
>
|
||||
<PlateEditor
|
||||
markdown={String(pendingEdits?.content ?? args.content)}
|
||||
readOnly
|
||||
preset="readonly"
|
||||
editorVariant="none"
|
||||
className="h-auto [&_[data-slate-editor]]:!min-h-0 [&_[data-slate-editor]>*:first-child]:!mt-0"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -329,12 +341,26 @@ function ApprovalCard({
|
|||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4 flex items-center gap-2 select-none">
|
||||
{allowedDecisions.includes("approve") && (
|
||||
<Button size="sm" className="rounded-lg gap-1.5" onClick={handleApprove} disabled={!canApprove || isPanelOpen}>
|
||||
<Button
|
||||
size="sm"
|
||||
className="rounded-lg gap-1.5"
|
||||
onClick={handleApprove}
|
||||
disabled={!canApprove || isPanelOpen}
|
||||
>
|
||||
Approve <CornerDownLeftIcon className="size-3 opacity-60" />
|
||||
</Button>
|
||||
)}
|
||||
{allowedDecisions.includes("reject") && (
|
||||
<Button size="sm" variant="ghost" className="rounded-lg text-muted-foreground" disabled={isPanelOpen} onClick={() => { setRejected(); onDecision({ type: "reject", message: "User rejected the action." }); }}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="rounded-lg text-muted-foreground"
|
||||
disabled={isPanelOpen}
|
||||
onClick={() => {
|
||||
setRejected();
|
||||
onDecision({ type: "reject", message: "User rejected the action." });
|
||||
}}
|
||||
>
|
||||
Reject
|
||||
</Button>
|
||||
)}
|
||||
|
|
@ -352,7 +378,9 @@ function ErrorCard({ result }: { result: ErrorResult }) {
|
|||
<p className="text-sm font-semibold text-destructive">Failed to create OneDrive file</p>
|
||||
</div>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4"><p className="text-sm text-muted-foreground">{result.message}</p></div>
|
||||
<div className="px-5 py-4">
|
||||
<p className="text-sm text-muted-foreground">{result.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -364,7 +392,9 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
|||
<p className="text-sm font-semibold text-destructive">OneDrive authentication expired</p>
|
||||
</div>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4"><p className="text-sm text-muted-foreground">{result.message}</p></div>
|
||||
<div className="px-5 py-4">
|
||||
<p className="text-sm text-muted-foreground">{result.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -373,7 +403,9 @@ function SuccessCard({ result }: { result: SuccessResult }) {
|
|||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||
<div className="px-5 pt-5 pb-4">
|
||||
<p className="text-sm font-semibold text-foreground">{result.message || "OneDrive file created successfully"}</p>
|
||||
<p className="text-sm font-semibold text-foreground">
|
||||
{result.message || "OneDrive file created successfully"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4 space-y-2 text-xs">
|
||||
|
|
@ -383,7 +415,14 @@ function SuccessCard({ result }: { result: SuccessResult }) {
|
|||
</div>
|
||||
{result.web_url && (
|
||||
<div>
|
||||
<a href={result.web_url} target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">Open in OneDrive</a>
|
||||
<a
|
||||
href={result.web_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
Open in OneDrive
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -391,12 +430,31 @@ function SuccessCard({ result }: { result: SuccessResult }) {
|
|||
);
|
||||
}
|
||||
|
||||
export const CreateOneDriveFileToolUI = ({ args, result }: ToolCallMessagePartProps<{ name: string; content?: string }, CreateOneDriveFileResult>) => {
|
||||
export const CreateOneDriveFileToolUI = ({
|
||||
args,
|
||||
result,
|
||||
}: ToolCallMessagePartProps<{ name: string; content?: string }, CreateOneDriveFileResult>) => {
|
||||
if (!result) return null;
|
||||
if (isInterruptResult(result)) {
|
||||
return <ApprovalCard args={args} interruptData={result} onDecision={(decision) => { window.dispatchEvent(new CustomEvent("hitl-decision", { detail: { decisions: [decision] } })); }} />;
|
||||
return (
|
||||
<ApprovalCard
|
||||
args={args}
|
||||
interruptData={result}
|
||||
onDecision={(decision) => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("hitl-decision", { detail: { decisions: [decision] } })
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (typeof result === "object" && result !== null && "status" in result && (result as { status: string }).status === "rejected") return null;
|
||||
if (
|
||||
typeof result === "object" &&
|
||||
result !== null &&
|
||||
"status" in result &&
|
||||
(result as { status: string }).status === "rejected"
|
||||
)
|
||||
return null;
|
||||
if (isAuthErrorResult(result)) return <AuthErrorCard result={result} />;
|
||||
if (isErrorResult(result)) return <ErrorCard result={result} />;
|
||||
return <SuccessCard result={result as SuccessResult} />;
|
||||
|
|
|
|||
|
|
@ -31,29 +31,76 @@ interface InterruptResult {
|
|||
context?: { account?: OneDriveAccount; file?: OneDriveFile; error?: string };
|
||||
}
|
||||
|
||||
interface SuccessResult { status: "success"; file_id: string; message?: string; deleted_from_kb?: boolean }
|
||||
interface ErrorResult { status: "error"; message: string }
|
||||
interface NotFoundResult { status: "not_found"; message: string }
|
||||
interface AuthErrorResult { status: "auth_error"; message: string; connector_type?: string }
|
||||
interface SuccessResult {
|
||||
status: "success";
|
||||
file_id: string;
|
||||
message?: string;
|
||||
deleted_from_kb?: boolean;
|
||||
}
|
||||
interface ErrorResult {
|
||||
status: "error";
|
||||
message: string;
|
||||
}
|
||||
interface NotFoundResult {
|
||||
status: "not_found";
|
||||
message: string;
|
||||
}
|
||||
interface AuthErrorResult {
|
||||
status: "auth_error";
|
||||
message: string;
|
||||
connector_type?: string;
|
||||
}
|
||||
|
||||
type DeleteOneDriveFileResult = InterruptResult | SuccessResult | ErrorResult | NotFoundResult | AuthErrorResult;
|
||||
type DeleteOneDriveFileResult =
|
||||
| InterruptResult
|
||||
| SuccessResult
|
||||
| ErrorResult
|
||||
| NotFoundResult
|
||||
| AuthErrorResult;
|
||||
|
||||
function isInterruptResult(result: unknown): result is InterruptResult {
|
||||
return typeof result === "object" && result !== null && "__interrupt__" in result && (result as InterruptResult).__interrupt__ === true;
|
||||
return (
|
||||
typeof result === "object" &&
|
||||
result !== null &&
|
||||
"__interrupt__" in result &&
|
||||
(result as InterruptResult).__interrupt__ === true
|
||||
);
|
||||
}
|
||||
function isErrorResult(result: unknown): result is ErrorResult {
|
||||
return typeof result === "object" && result !== null && "status" in result && (result as ErrorResult).status === "error";
|
||||
return (
|
||||
typeof result === "object" &&
|
||||
result !== null &&
|
||||
"status" in result &&
|
||||
(result as ErrorResult).status === "error"
|
||||
);
|
||||
}
|
||||
function isNotFoundResult(result: unknown): result is NotFoundResult {
|
||||
return typeof result === "object" && result !== null && "status" in result && (result as NotFoundResult).status === "not_found";
|
||||
return (
|
||||
typeof result === "object" &&
|
||||
result !== null &&
|
||||
"status" in result &&
|
||||
(result as NotFoundResult).status === "not_found"
|
||||
);
|
||||
}
|
||||
function isAuthErrorResult(result: unknown): result is AuthErrorResult {
|
||||
return typeof result === "object" && result !== null && "status" in result && (result as AuthErrorResult).status === "auth_error";
|
||||
return (
|
||||
typeof result === "object" &&
|
||||
result !== null &&
|
||||
"status" in result &&
|
||||
(result as AuthErrorResult).status === "auth_error"
|
||||
);
|
||||
}
|
||||
|
||||
function ApprovalCard({ interruptData, onDecision }: {
|
||||
function ApprovalCard({
|
||||
interruptData,
|
||||
onDecision,
|
||||
}: {
|
||||
interruptData: InterruptResult;
|
||||
onDecision: (decision: { type: "approve" | "reject"; message?: string; edited_action?: { name: string; args: Record<string, unknown> } }) => void;
|
||||
onDecision: (decision: {
|
||||
type: "approve" | "reject";
|
||||
message?: string;
|
||||
edited_action?: { name: string; args: Record<string, unknown> };
|
||||
}) => void;
|
||||
}) {
|
||||
const { phase, setProcessing, setRejected } = useHitlPhase(interruptData);
|
||||
const [deleteFromKb, setDeleteFromKb] = useState(false);
|
||||
|
|
@ -87,7 +134,11 @@ function ApprovalCard({ interruptData, onDecision }: {
|
|||
<div className="flex items-start justify-between px-5 pt-5 pb-4 select-none">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-foreground">
|
||||
{phase === "rejected" ? "OneDrive File Deletion Rejected" : phase === "processing" || phase === "complete" ? "OneDrive File Deletion Approved" : "Delete OneDrive File"}
|
||||
{phase === "rejected"
|
||||
? "OneDrive File Deletion Rejected"
|
||||
: phase === "processing" || phase === "complete"
|
||||
? "OneDrive File Deletion Approved"
|
||||
: "Delete OneDrive File"}
|
||||
</p>
|
||||
{phase === "processing" ? (
|
||||
<TextShimmerLoader text="Trashing file" size="sm" />
|
||||
|
|
@ -96,7 +147,9 @@ function ApprovalCard({ interruptData, onDecision }: {
|
|||
) : phase === "rejected" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">File deletion was cancelled</p>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">Requires your approval to proceed</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Requires your approval to proceed
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -112,7 +165,9 @@ function ApprovalCard({ interruptData, onDecision }: {
|
|||
{account && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-medium text-muted-foreground">OneDrive Account</p>
|
||||
<div className="w-full rounded-md border border-input bg-muted/50 px-3 py-2 text-sm">{account.name}</div>
|
||||
<div className="w-full rounded-md border border-input bg-muted/50 px-3 py-2 text-sm">
|
||||
{account.name}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{file && (
|
||||
|
|
@ -121,7 +176,14 @@ function ApprovalCard({ interruptData, onDecision }: {
|
|||
<div className="w-full rounded-md border border-input bg-muted/50 px-3 py-2 text-sm space-y-0.5">
|
||||
<div className="font-medium">{file.name}</div>
|
||||
{file.web_url && (
|
||||
<a href={file.web_url} target="_blank" rel="noopener noreferrer" className="text-xs text-primary hover:underline">Open in OneDrive</a>
|
||||
<a
|
||||
href={file.web_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-primary hover:underline"
|
||||
>
|
||||
Open in OneDrive
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -136,12 +198,21 @@ function ApprovalCard({ interruptData, onDecision }: {
|
|||
<>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4 space-y-3 select-none">
|
||||
<p className="text-xs text-muted-foreground">The file will be moved to the OneDrive recycle bin. You can restore it within 93 days.</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
The file will be moved to the OneDrive recycle bin. You can restore it within 93 days.
|
||||
</p>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Checkbox id="od-delete-from-kb" checked={deleteFromKb} onCheckedChange={(v) => setDeleteFromKb(v === true)} className="shrink-0" />
|
||||
<Checkbox
|
||||
id="od-delete-from-kb"
|
||||
checked={deleteFromKb}
|
||||
onCheckedChange={(v) => setDeleteFromKb(v === true)}
|
||||
className="shrink-0"
|
||||
/>
|
||||
<label htmlFor="od-delete-from-kb" className="flex-1 cursor-pointer">
|
||||
<span className="text-sm text-foreground">Also remove from knowledge base</span>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">This will permanently delete the file from your knowledge base</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
This will permanently delete the file from your knowledge base
|
||||
</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -152,8 +223,20 @@ function ApprovalCard({ interruptData, onDecision }: {
|
|||
<>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4 flex items-center gap-2 select-none">
|
||||
<Button size="sm" className="rounded-lg gap-1.5" onClick={handleApprove}>Approve <CornerDownLeftIcon className="size-3 opacity-60" /></Button>
|
||||
<Button size="sm" variant="ghost" className="rounded-lg text-muted-foreground" onClick={() => { setRejected(); onDecision({ type: "reject", message: "User rejected the action." }); }}>Reject</Button>
|
||||
<Button size="sm" className="rounded-lg gap-1.5" onClick={handleApprove}>
|
||||
Approve <CornerDownLeftIcon className="size-3 opacity-60" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="rounded-lg text-muted-foreground"
|
||||
onClick={() => {
|
||||
setRejected();
|
||||
onDecision({ type: "reject", message: "User rejected the action." });
|
||||
}}
|
||||
>
|
||||
Reject
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -164,9 +247,13 @@ function ApprovalCard({ interruptData, onDecision }: {
|
|||
function ErrorCard({ result }: { result: ErrorResult }) {
|
||||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||
<div className="px-5 pt-5 pb-4"><p className="text-sm font-semibold text-destructive">Failed to delete file</p></div>
|
||||
<div className="px-5 pt-5 pb-4">
|
||||
<p className="text-sm font-semibold text-destructive">Failed to delete file</p>
|
||||
</div>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4"><p className="text-sm text-muted-foreground">{result.message}</p></div>
|
||||
<div className="px-5 py-4">
|
||||
<p className="text-sm text-muted-foreground">{result.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -185,9 +272,13 @@ function NotFoundCard({ result }: { result: NotFoundResult }) {
|
|||
function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
||||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||
<div className="px-5 pt-5 pb-4"><p className="text-sm font-semibold text-destructive">OneDrive authentication expired</p></div>
|
||||
<div className="px-5 pt-5 pb-4">
|
||||
<p className="text-sm font-semibold text-destructive">OneDrive authentication expired</p>
|
||||
</div>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4"><p className="text-sm text-muted-foreground">{result.message}</p></div>
|
||||
<div className="px-5 py-4">
|
||||
<p className="text-sm text-muted-foreground">{result.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -195,23 +286,51 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
|||
function SuccessCard({ result }: { result: SuccessResult }) {
|
||||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||
<div className="px-5 pt-5 pb-4"><p className="text-sm font-semibold text-foreground">{result.message || "File moved to recycle bin"}</p></div>
|
||||
<div className="px-5 pt-5 pb-4">
|
||||
<p className="text-sm font-semibold text-foreground">
|
||||
{result.message || "File moved to recycle bin"}
|
||||
</p>
|
||||
</div>
|
||||
{result.deleted_from_kb && (
|
||||
<>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4 text-xs"><span className="text-green-600 dark:text-green-500">Also removed from knowledge base</span></div>
|
||||
<div className="px-5 py-4 text-xs">
|
||||
<span className="text-green-600 dark:text-green-500">
|
||||
Also removed from knowledge base
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const DeleteOneDriveFileToolUI = ({ result }: ToolCallMessagePartProps<{ file_name: string; delete_from_kb?: boolean }, DeleteOneDriveFileResult>) => {
|
||||
export const DeleteOneDriveFileToolUI = ({
|
||||
result,
|
||||
}: ToolCallMessagePartProps<
|
||||
{ file_name: string; delete_from_kb?: boolean },
|
||||
DeleteOneDriveFileResult
|
||||
>) => {
|
||||
if (!result) return null;
|
||||
if (isInterruptResult(result)) {
|
||||
return <ApprovalCard interruptData={result} onDecision={(decision) => { window.dispatchEvent(new CustomEvent("hitl-decision", { detail: { decisions: [decision] } })); }} />;
|
||||
return (
|
||||
<ApprovalCard
|
||||
interruptData={result}
|
||||
onDecision={(decision) => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("hitl-decision", { detail: { decisions: [decision] } })
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (typeof result === "object" && result !== null && "status" in result && (result as { status: string }).status === "rejected") return null;
|
||||
if (
|
||||
typeof result === "object" &&
|
||||
result !== null &&
|
||||
"status" in result &&
|
||||
(result as { status: string }).status === "rejected"
|
||||
)
|
||||
return null;
|
||||
if (isAuthErrorResult(result)) return <AuthErrorCard result={result} />;
|
||||
if (isNotFoundResult(result)) return <NotFoundCard result={result} />;
|
||||
if (isErrorResult(result)) return <ErrorCard result={result} />;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue