mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-06 14:22:47 +02:00
Stabilize HITL bundle UX and resume.
This commit is contained in:
parent
972650909c
commit
0af2c28a8d
12 changed files with 553 additions and 184 deletions
|
|
@ -40,6 +40,7 @@ import { ToolFallback } from "@/components/assistant-ui/tool-fallback";
|
|||
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
||||
import { CommentPanelContainer } from "@/components/chat-comments/comment-panel-container/comment-panel-container";
|
||||
import { CommentSheet } from "@/components/chat-comments/comment-sheet/comment-sheet";
|
||||
import { withBundleStep } from "@/components/hitl-bundle-pager";
|
||||
import type { SerializableCitation } from "@/components/tool-ui/citation";
|
||||
import {
|
||||
openSafeNavigationHref,
|
||||
|
|
@ -484,6 +485,51 @@ const MessageInfoDropdown: FC = () => {
|
|||
);
|
||||
};
|
||||
|
||||
// Wrap each tool-ui card with ``withBundleStep`` so multi-card HITL bundles
|
||||
// page through them and stage decisions instead of firing one resume per card.
|
||||
const TOOLS_BY_NAME = {
|
||||
generate_report: withBundleStep(GenerateReportToolUI),
|
||||
generate_resume: withBundleStep(GenerateResumeToolUI),
|
||||
generate_podcast: withBundleStep(GeneratePodcastToolUI),
|
||||
generate_video_presentation: withBundleStep(GenerateVideoPresentationToolUI),
|
||||
display_image: withBundleStep(GenerateImageToolUI),
|
||||
generate_image: withBundleStep(GenerateImageToolUI),
|
||||
update_memory: withBundleStep(UpdateMemoryToolUI),
|
||||
execute: withBundleStep(SandboxExecuteToolUI),
|
||||
execute_code: withBundleStep(SandboxExecuteToolUI),
|
||||
create_notion_page: withBundleStep(CreateNotionPageToolUI),
|
||||
update_notion_page: withBundleStep(UpdateNotionPageToolUI),
|
||||
delete_notion_page: withBundleStep(DeleteNotionPageToolUI),
|
||||
create_linear_issue: withBundleStep(CreateLinearIssueToolUI),
|
||||
update_linear_issue: withBundleStep(UpdateLinearIssueToolUI),
|
||||
delete_linear_issue: withBundleStep(DeleteLinearIssueToolUI),
|
||||
create_google_drive_file: withBundleStep(CreateGoogleDriveFileToolUI),
|
||||
delete_google_drive_file: withBundleStep(DeleteGoogleDriveFileToolUI),
|
||||
create_onedrive_file: withBundleStep(CreateOneDriveFileToolUI),
|
||||
delete_onedrive_file: withBundleStep(DeleteOneDriveFileToolUI),
|
||||
create_dropbox_file: withBundleStep(CreateDropboxFileToolUI),
|
||||
delete_dropbox_file: withBundleStep(DeleteDropboxFileToolUI),
|
||||
create_calendar_event: withBundleStep(CreateCalendarEventToolUI),
|
||||
update_calendar_event: withBundleStep(UpdateCalendarEventToolUI),
|
||||
delete_calendar_event: withBundleStep(DeleteCalendarEventToolUI),
|
||||
create_gmail_draft: withBundleStep(CreateGmailDraftToolUI),
|
||||
update_gmail_draft: withBundleStep(UpdateGmailDraftToolUI),
|
||||
send_gmail_email: withBundleStep(SendGmailEmailToolUI),
|
||||
trash_gmail_email: withBundleStep(TrashGmailEmailToolUI),
|
||||
create_jira_issue: withBundleStep(CreateJiraIssueToolUI),
|
||||
update_jira_issue: withBundleStep(UpdateJiraIssueToolUI),
|
||||
delete_jira_issue: withBundleStep(DeleteJiraIssueToolUI),
|
||||
create_confluence_page: withBundleStep(CreateConfluencePageToolUI),
|
||||
update_confluence_page: withBundleStep(UpdateConfluencePageToolUI),
|
||||
delete_confluence_page: withBundleStep(DeleteConfluencePageToolUI),
|
||||
web_search: () => null,
|
||||
link_preview: () => null,
|
||||
multi_link_preview: () => null,
|
||||
scrape_webpage: () => null,
|
||||
} as const;
|
||||
|
||||
const TOOLS_FALLBACK = withBundleStep(ToolFallback);
|
||||
|
||||
const AssistantMessageInner: FC = () => {
|
||||
const isMobile = !useMediaQuery("(min-width: 768px)");
|
||||
|
||||
|
|
@ -495,47 +541,8 @@ const AssistantMessageInner: FC = () => {
|
|||
Text: MarkdownText,
|
||||
Reasoning: ReasoningMessagePart,
|
||||
tools: {
|
||||
by_name: {
|
||||
generate_report: GenerateReportToolUI,
|
||||
generate_resume: GenerateResumeToolUI,
|
||||
generate_podcast: GeneratePodcastToolUI,
|
||||
generate_video_presentation: GenerateVideoPresentationToolUI,
|
||||
display_image: GenerateImageToolUI,
|
||||
generate_image: GenerateImageToolUI,
|
||||
update_memory: UpdateMemoryToolUI,
|
||||
execute: SandboxExecuteToolUI,
|
||||
execute_code: SandboxExecuteToolUI,
|
||||
create_notion_page: CreateNotionPageToolUI,
|
||||
update_notion_page: UpdateNotionPageToolUI,
|
||||
delete_notion_page: DeleteNotionPageToolUI,
|
||||
create_linear_issue: CreateLinearIssueToolUI,
|
||||
update_linear_issue: UpdateLinearIssueToolUI,
|
||||
delete_linear_issue: DeleteLinearIssueToolUI,
|
||||
create_google_drive_file: CreateGoogleDriveFileToolUI,
|
||||
delete_google_drive_file: DeleteGoogleDriveFileToolUI,
|
||||
create_onedrive_file: CreateOneDriveFileToolUI,
|
||||
delete_onedrive_file: DeleteOneDriveFileToolUI,
|
||||
create_dropbox_file: CreateDropboxFileToolUI,
|
||||
delete_dropbox_file: DeleteDropboxFileToolUI,
|
||||
create_calendar_event: CreateCalendarEventToolUI,
|
||||
update_calendar_event: UpdateCalendarEventToolUI,
|
||||
delete_calendar_event: DeleteCalendarEventToolUI,
|
||||
create_gmail_draft: CreateGmailDraftToolUI,
|
||||
update_gmail_draft: UpdateGmailDraftToolUI,
|
||||
send_gmail_email: SendGmailEmailToolUI,
|
||||
trash_gmail_email: TrashGmailEmailToolUI,
|
||||
create_jira_issue: CreateJiraIssueToolUI,
|
||||
update_jira_issue: UpdateJiraIssueToolUI,
|
||||
delete_jira_issue: DeleteJiraIssueToolUI,
|
||||
create_confluence_page: CreateConfluencePageToolUI,
|
||||
update_confluence_page: UpdateConfluencePageToolUI,
|
||||
delete_confluence_page: DeleteConfluencePageToolUI,
|
||||
web_search: () => null,
|
||||
link_preview: () => null,
|
||||
multi_link_preview: () => null,
|
||||
scrape_webpage: () => null,
|
||||
},
|
||||
Fallback: ToolFallback,
|
||||
by_name: TOOLS_BY_NAME,
|
||||
Fallback: TOOLS_FALLBACK,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
2
surfsense_web/components/hitl-bundle-pager/index.ts
Normal file
2
surfsense_web/components/hitl-bundle-pager/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { PagerChrome } from "./pager-chrome";
|
||||
export { withBundleStep } from "./with-bundle-step";
|
||||
61
surfsense_web/components/hitl-bundle-pager/pager-chrome.tsx
Normal file
61
surfsense_web/components/hitl-bundle-pager/pager-chrome.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
"use client";
|
||||
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useHitlBundle } from "@/lib/hitl";
|
||||
|
||||
/**
|
||||
* Prev/next nav and Submit for the current step of an active HITL bundle.
|
||||
* Submission is gated on every action_request having a staged decision.
|
||||
*/
|
||||
export function PagerChrome() {
|
||||
const bundle = useHitlBundle();
|
||||
if (!bundle) return null;
|
||||
|
||||
const total = bundle.toolCallIds.length;
|
||||
const step = bundle.currentStep;
|
||||
const allStaged = bundle.stagedCount === total;
|
||||
|
||||
return (
|
||||
<div className="mt-3 flex items-center gap-2 rounded-md border border-border bg-muted/40 p-2 text-sm">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={bundle.prev}
|
||||
disabled={step === 0}
|
||||
aria-label="Previous approval"
|
||||
>
|
||||
<ChevronLeftIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<span className="font-medium tabular-nums">
|
||||
{step + 1} / {total}
|
||||
</span>
|
||||
<span className="text-muted-foreground">·</span>
|
||||
<span className="text-muted-foreground">
|
||||
{bundle.stagedCount} of {total} decided
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={bundle.next}
|
||||
disabled={step >= total - 1}
|
||||
aria-label="Next approval"
|
||||
>
|
||||
<ChevronRightIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<div className="ml-auto">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
onClick={bundle.submit}
|
||||
disabled={!allStaged}
|
||||
title={allStaged ? "Submit decisions" : "Decide every action first"}
|
||||
>
|
||||
Submit decisions
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
"use client";
|
||||
|
||||
import type { ToolCallMessagePartProps } from "@assistant-ui/react";
|
||||
import type { ComponentType } from "react";
|
||||
import { ToolCallIdProvider, useHitlBundle } from "@/lib/hitl";
|
||||
import { PagerChrome } from "./pager-chrome";
|
||||
|
||||
/**
|
||||
* Wrap a tool-ui card so that, when a multi-card HITL bundle is active:
|
||||
* - cards belonging to the bundle but not the current step render ``null``;
|
||||
* - the current-step card renders normally and is followed by ``PagerChrome``.
|
||||
*
|
||||
* Cards stay completely unchanged — the wrapper provides the
|
||||
* ``ToolCallIdContext`` that ``useHitlDecision`` reads to stage decisions
|
||||
* against the right ``toolCallId`` instead of firing the global event.
|
||||
*/
|
||||
export function withBundleStep<P extends ToolCallMessagePartProps<any, any>>(
|
||||
Component: ComponentType<P>
|
||||
): ComponentType<P> {
|
||||
function BundleStepWrapped(props: P) {
|
||||
const bundle = useHitlBundle();
|
||||
const toolCallId = props.toolCallId;
|
||||
const inBundle = bundle?.isInBundle(toolCallId) ?? false;
|
||||
const isStep = bundle?.isCurrentStep(toolCallId) ?? false;
|
||||
|
||||
if (bundle && inBundle && !isStep) return null;
|
||||
|
||||
return (
|
||||
<ToolCallIdProvider toolCallId={toolCallId}>
|
||||
<Component {...props} />
|
||||
{bundle && isStep ? <PagerChrome /> : null}
|
||||
</ToolCallIdProvider>
|
||||
);
|
||||
}
|
||||
BundleStepWrapped.displayName = `withBundleStep(${Component.displayName ?? Component.name ?? "ToolUI"})`;
|
||||
return BundleStepWrapped as ComponentType<P>;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue