mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-12 09:12:40 +02:00
add HITL tool argument editing with approval UI
Enables users to edit tool call arguments before execution in human-in-the-loop workflows. Adds edit mode UI with form fields, grayscale styling, and subtle pulse animations for pending approvals. Backend stub enhanced to verify edited arguments are correctly passed through.
This commit is contained in:
parent
5d1c386105
commit
2ef2474058
5 changed files with 216 additions and 71 deletions
|
|
@ -1,3 +1,4 @@
|
||||||
|
import hashlib
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from langchain_core.tools import tool
|
from langchain_core.tools import tool
|
||||||
|
|
@ -19,11 +20,19 @@ def create_create_notion_page_tool():
|
||||||
title: The title of the Notion page.
|
title: The title of the Notion page.
|
||||||
content: The markdown content for the page body.
|
content: The markdown content for the page body.
|
||||||
"""
|
"""
|
||||||
|
# Generate a unique page ID based on title for testing
|
||||||
|
# This helps verify if edited args were used
|
||||||
|
page_hash = hashlib.md5(title.encode()).hexdigest()[:8]
|
||||||
|
|
||||||
|
# Return detailed response showing what was actually received
|
||||||
return {
|
return {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"page_id": "stub-page-id-12345",
|
"page_id": f"stub-page-{page_hash}",
|
||||||
"title": title,
|
"title": title,
|
||||||
"url": "https://www.notion.so/stub-page-12345",
|
"content_preview": content[:100] + "..." if len(content) > 100 else content,
|
||||||
|
"content_length": len(content),
|
||||||
|
"url": f"https://www.notion.so/stub-page-{page_hash}",
|
||||||
|
"message": f"✅ Created Notion page '{title}' with {len(content)} characters",
|
||||||
}
|
}
|
||||||
|
|
||||||
return create_notion_page
|
return create_notion_page
|
||||||
|
|
|
||||||
|
|
@ -864,7 +864,7 @@ export default function NewChatPage() {
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleResume = useCallback(
|
const handleResume = useCallback(
|
||||||
async (decisions: Array<{ type: string; message?: string }>) => {
|
async (decisions: Array<{ type: string; message?: string; edited_action?: { name: string; args: Record<string, unknown> } }>) => {
|
||||||
if (!pendingInterrupt) return;
|
if (!pendingInterrupt) return;
|
||||||
const { threadId: resumeThreadId, assistantMsgId } = pendingInterrupt;
|
const { threadId: resumeThreadId, assistantMsgId } = pendingInterrupt;
|
||||||
setPendingInterrupt(null);
|
setPendingInterrupt(null);
|
||||||
|
|
@ -1098,10 +1098,16 @@ export default function NewChatPage() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (e: Event) => {
|
const handler = (e: Event) => {
|
||||||
const detail = (e as CustomEvent).detail as {
|
const detail = (e as CustomEvent).detail as {
|
||||||
decisions: Array<{ type: string; message?: string }>;
|
decisions: Array<{
|
||||||
|
type: string;
|
||||||
|
message?: string;
|
||||||
|
edited_action?: { name: string; args: Record<string, unknown> };
|
||||||
|
}>;
|
||||||
};
|
};
|
||||||
if (detail?.decisions && pendingInterrupt) {
|
if (detail?.decisions && pendingInterrupt) {
|
||||||
const decisionType = detail.decisions[0]?.type as "approve" | "reject";
|
const decision = detail.decisions[0];
|
||||||
|
const decisionType = decision?.type as "approve" | "reject" | "edit";
|
||||||
|
|
||||||
setMessages((prev) =>
|
setMessages((prev) =>
|
||||||
prev.map((m) => {
|
prev.map((m) => {
|
||||||
if (m.id !== pendingInterrupt.assistantMsgId) return m;
|
if (m.id !== pendingInterrupt.assistantMsgId) return m;
|
||||||
|
|
@ -1113,9 +1119,23 @@ export default function NewChatPage() {
|
||||||
part.result !== null &&
|
part.result !== null &&
|
||||||
"__interrupt__" in part.result
|
"__interrupt__" in part.result
|
||||||
) {
|
) {
|
||||||
|
// For edit decisions, also update the displayed args
|
||||||
|
if (decisionType === "edit" && decision.edited_action) {
|
||||||
|
return {
|
||||||
|
...part,
|
||||||
|
args: decision.edited_action.args, // Update displayed args
|
||||||
|
result: {
|
||||||
|
...(part.result as Record<string, unknown>),
|
||||||
|
__decided__: decisionType
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...part,
|
...part,
|
||||||
result: { ...(part.result as Record<string, unknown>), __decided__: decisionType },
|
result: {
|
||||||
|
...(part.result as Record<string, unknown>),
|
||||||
|
__decided__: decisionType
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return part;
|
return part;
|
||||||
|
|
|
||||||
|
|
@ -187,5 +187,21 @@ button {
|
||||||
background-color: hsl(var(--muted-foreground) / 0.4);
|
background-color: hsl(var(--muted-foreground) / 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Human-in-the-loop approval card animations */
|
||||||
|
@keyframes pulse-subtle {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
box-shadow: 0 0 0 0 rgb(0 0 0 / 0.15);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
box-shadow: 0 0 20px 4px rgb(0 0 0 / 0.12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-pulse-subtle {
|
||||||
|
animation: pulse-subtle 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
@source '../node_modules/@llamaindex/chat-ui/**/*.{ts,tsx}';
|
@source '../node_modules/@llamaindex/chat-ui/**/*.{ts,tsx}';
|
||||||
@source '../node_modules/streamdown/dist/*.js';
|
@source '../node_modules/streamdown/dist/*.js';
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||||
import { CheckIcon, FileTextIcon, Loader2Icon, XIcon } from "lucide-react";
|
import { AlertTriangleIcon, CheckIcon, FileTextIcon, Loader2Icon, PencilIcon, XIcon } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
|
||||||
interface InterruptResult {
|
interface InterruptResult {
|
||||||
__interrupt__: true;
|
__interrupt__: true;
|
||||||
__decided__?: "approve" | "reject";
|
__decided__?: "approve" | "reject" | "edit";
|
||||||
action_requests: Array<{
|
action_requests: Array<{
|
||||||
name: string;
|
name: string;
|
||||||
args: Record<string, unknown>;
|
args: Record<string, unknown>;
|
||||||
|
|
@ -24,6 +26,9 @@ interface SuccessResult {
|
||||||
page_id: string;
|
page_id: string;
|
||||||
title: string;
|
title: string;
|
||||||
url: string;
|
url: string;
|
||||||
|
content_preview?: string;
|
||||||
|
content_length?: number;
|
||||||
|
message?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateNotionPageResult = InterruptResult | SuccessResult;
|
type CreateNotionPageResult = InterruptResult | SuccessResult;
|
||||||
|
|
@ -44,50 +49,114 @@ function ApprovalCard({
|
||||||
}: {
|
}: {
|
||||||
args: Record<string, unknown>;
|
args: Record<string, unknown>;
|
||||||
interruptData: InterruptResult;
|
interruptData: InterruptResult;
|
||||||
onDecision: (decision: { type: "approve" | "reject"; message?: string }) => void;
|
onDecision: (decision: { type: "approve" | "reject" | "edit"; message?: string; edited_action?: { name: string; args: Record<string, unknown> } }) => void;
|
||||||
}) {
|
}) {
|
||||||
const [decided, setDecided] = useState<"approve" | "reject" | null>(
|
const [decided, setDecided] = useState<"approve" | "reject" | "edit" | null>(
|
||||||
interruptData.__decided__ ?? null
|
interruptData.__decided__ ?? null
|
||||||
);
|
);
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const [editedArgs, setEditedArgs] = useState<Record<string, unknown>>(args);
|
||||||
|
|
||||||
const reviewConfig = interruptData.review_configs[0];
|
const reviewConfig = interruptData.review_configs[0];
|
||||||
const allowedDecisions = reviewConfig?.allowed_decisions ?? ["approve", "reject"];
|
const allowedDecisions = reviewConfig?.allowed_decisions ?? ["approve", "reject"];
|
||||||
|
const canEdit = allowedDecisions.includes("edit");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="my-4 max-w-md overflow-hidden rounded-xl border border-border bg-card">
|
<div className={`my-4 max-w-full overflow-hidden rounded-xl transition-all duration-300 ${
|
||||||
<div className="flex items-center gap-3 border-b border-border px-4 py-3">
|
decided
|
||||||
<div className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-primary/10">
|
? "border border-border bg-card shadow-sm"
|
||||||
<FileTextIcon className="size-4 text-primary" />
|
: "border-2 border-foreground/20 bg-muted/30 dark:bg-muted/10 shadow-lg animate-pulse-subtle"
|
||||||
|
}`}>
|
||||||
|
<div className={`flex items-center gap-3 border-b ${
|
||||||
|
decided
|
||||||
|
? "border-border bg-card"
|
||||||
|
: "border-foreground/15 bg-muted/40 dark:bg-muted/20"
|
||||||
|
} px-4 py-3`}>
|
||||||
|
<div className={`flex size-9 shrink-0 items-center justify-center rounded-lg ${
|
||||||
|
decided
|
||||||
|
? "bg-muted"
|
||||||
|
: "bg-muted animate-pulse"
|
||||||
|
}`}>
|
||||||
|
<AlertTriangleIcon className={`size-4 ${
|
||||||
|
decided
|
||||||
|
? "text-muted-foreground"
|
||||||
|
: "text-foreground"
|
||||||
|
}`} />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="text-sm font-medium">Create Notion Page</p>
|
<p className={`text-sm font-medium ${
|
||||||
<p className="truncate text-xs text-muted-foreground">
|
decided
|
||||||
Requires your approval to proceed
|
? "text-foreground"
|
||||||
|
: "text-foreground"
|
||||||
|
}`}>Create Notion Page</p>
|
||||||
|
<p className={`truncate text-xs ${
|
||||||
|
decided
|
||||||
|
? "text-muted-foreground"
|
||||||
|
: "text-muted-foreground"
|
||||||
|
}`}>
|
||||||
|
{isEditing ? "You can edit the arguments below" : "Requires your approval to proceed"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2 px-4 py-3">
|
{/* Display mode - show args as read-only */}
|
||||||
{args.title != null && (
|
{!isEditing && (
|
||||||
<div>
|
<div className="space-y-2 px-4 py-3 bg-card">
|
||||||
<p className="text-xs font-medium text-muted-foreground">Title</p>
|
{args.title != null && (
|
||||||
<p className="text-sm">{String(args.title)}</p>
|
<div>
|
||||||
|
<p className="text-xs font-medium text-muted-foreground">Title</p>
|
||||||
|
<p className="text-sm text-foreground">{String(args.title)}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{args.content != null && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium text-muted-foreground">Content</p>
|
||||||
|
<p className="line-clamp-4 text-sm whitespace-pre-wrap text-foreground">{String(args.content)}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{args.content != null && (
|
|
||||||
<div>
|
|
||||||
<p className="text-xs font-medium text-muted-foreground">Content</p>
|
|
||||||
<p className="line-clamp-4 text-sm whitespace-pre-wrap">{String(args.content)}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2 border-t border-border px-4 py-3">
|
{/* Edit mode - show editable form fields */}
|
||||||
|
{isEditing && (
|
||||||
|
<div className="space-y-3 px-4 py-3 bg-card">
|
||||||
|
<div>
|
||||||
|
<label className="text-xs font-medium text-muted-foreground mb-1.5 block">
|
||||||
|
Title
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={String(editedArgs.title ?? "")}
|
||||||
|
onChange={(e) => setEditedArgs({ ...editedArgs, title: e.target.value })}
|
||||||
|
placeholder="Enter page title"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-xs font-medium text-muted-foreground mb-1.5 block">
|
||||||
|
Content
|
||||||
|
</label>
|
||||||
|
<Textarea
|
||||||
|
value={String(editedArgs.content ?? "")}
|
||||||
|
onChange={(e) => setEditedArgs({ ...editedArgs, content: e.target.value })}
|
||||||
|
placeholder="Enter page content"
|
||||||
|
rows={6}
|
||||||
|
className="resize-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Action buttons */}
|
||||||
|
<div className={`flex items-center gap-2 border-t ${
|
||||||
|
decided
|
||||||
|
? "border-border bg-card"
|
||||||
|
: "border-foreground/15 bg-muted/20 dark:bg-muted/10"
|
||||||
|
} px-4 py-3`}>
|
||||||
{decided ? (
|
{decided ? (
|
||||||
<p className="flex items-center gap-1.5 text-sm text-muted-foreground">
|
<p className="flex items-center gap-1.5 text-sm text-muted-foreground">
|
||||||
{decided === "approve" ? (
|
{decided === "approve" || decided === "edit" ? (
|
||||||
<>
|
<>
|
||||||
<CheckIcon className="size-3.5 text-green-500" />
|
<CheckIcon className="size-3.5 text-green-500" />
|
||||||
Approved
|
{decided === "edit" ? "Approved with Changes" : "Approved"}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|
@ -96,6 +165,35 @@ function ApprovalCard({
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
) : isEditing ? (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setDecided("edit");
|
||||||
|
onDecision({
|
||||||
|
type: "edit",
|
||||||
|
edited_action: {
|
||||||
|
name: interruptData.action_requests[0].name,
|
||||||
|
args: editedArgs,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CheckIcon />
|
||||||
|
Approve with Changes
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setIsEditing(false);
|
||||||
|
setEditedArgs(args); // Reset to original args
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{allowedDecisions.includes("approve") && (
|
{allowedDecisions.includes("approve") && (
|
||||||
|
|
@ -110,6 +208,16 @@ function ApprovalCard({
|
||||||
Approve
|
Approve
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{canEdit && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setIsEditing(true)}
|
||||||
|
>
|
||||||
|
<PencilIcon />
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{allowedDecisions.includes("reject") && (
|
{allowedDecisions.includes("reject") && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -133,15 +241,37 @@ function ApprovalCard({
|
||||||
function SuccessCard({ result }: { result: SuccessResult }) {
|
function SuccessCard({ result }: { result: SuccessResult }) {
|
||||||
return (
|
return (
|
||||||
<div className="my-4 max-w-md overflow-hidden rounded-xl border border-border bg-card">
|
<div className="my-4 max-w-md overflow-hidden rounded-xl border border-border bg-card">
|
||||||
<div className="flex items-center gap-3 px-4 py-3">
|
<div className="flex items-center gap-3 border-b border-border px-4 py-3">
|
||||||
<div className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-green-500/10">
|
<div className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-green-500/10">
|
||||||
<CheckIcon className="size-4 text-green-500" />
|
<CheckIcon className="size-4 text-green-500" />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="text-sm font-medium">{result.title}</p>
|
<p className="text-sm font-medium">{result.title}</p>
|
||||||
<p className="text-xs text-muted-foreground">Notion page created</p>
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{result.message || "Notion page created successfully"}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Show details to verify the arguments were used */}
|
||||||
|
<div className="space-y-2 px-4 py-3 text-xs">
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-muted-foreground">Page ID: </span>
|
||||||
|
<span className="font-mono">{result.page_id}</span>
|
||||||
|
</div>
|
||||||
|
{result.content_length != null && (
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-muted-foreground">Content: </span>
|
||||||
|
<span>{result.content_length} characters</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{result.content_preview && (
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-muted-foreground">Preview: </span>
|
||||||
|
<span className="text-muted-foreground italic">{result.content_preview}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,5 @@
|
||||||
import type { ThreadMessageLike } from "@assistant-ui/react";
|
import type { ThreadMessageLike } from "@assistant-ui/react";
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracted from page.tsx lines 131-136.
|
|
||||||
* Used across onNew, handleResume, and handleRegenerate.
|
|
||||||
*/
|
|
||||||
export interface ThinkingStepData {
|
export interface ThinkingStepData {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -11,10 +7,7 @@ export interface ThinkingStepData {
|
||||||
items: string[];
|
items: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracted from page.tsx lines 537-545.
|
|
||||||
* Duplicated in onNew, handleResume, and handleRegenerate.
|
|
||||||
*/
|
|
||||||
export type ContentPart =
|
export type ContentPart =
|
||||||
| { type: "text"; text: string }
|
| { type: "text"; text: string }
|
||||||
| {
|
| {
|
||||||
|
|
@ -25,21 +18,14 @@ export type ContentPart =
|
||||||
result?: unknown;
|
result?: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Mutable state shared by the content-part helpers (appendText, addToolCall, etc.).
|
|
||||||
* All handlers create this same set of variables -- this groups them into one object
|
|
||||||
* so helpers can read/write them by reference.
|
|
||||||
*/
|
|
||||||
export interface ContentPartsState {
|
export interface ContentPartsState {
|
||||||
contentParts: ContentPart[];
|
contentParts: ContentPart[];
|
||||||
currentTextPartIndex: number;
|
currentTextPartIndex: number;
|
||||||
toolCallIndices: Map<string, number>;
|
toolCallIndices: Map<string, number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracted from page.tsx lines 556-573 (onNew).
|
|
||||||
* Identical in handleResume (lines 1057-1064) and handleRegenerate (lines 1445-1452).
|
|
||||||
*/
|
|
||||||
export function appendText(state: ContentPartsState, delta: string): void {
|
export function appendText(state: ContentPartsState, delta: string): void {
|
||||||
if (state.currentTextPartIndex >= 0 && state.contentParts[state.currentTextPartIndex]?.type === "text") {
|
if (state.currentTextPartIndex >= 0 && state.contentParts[state.currentTextPartIndex]?.type === "text") {
|
||||||
(state.contentParts[state.currentTextPartIndex] as { type: "text"; text: string }).text += delta;
|
(state.contentParts[state.currentTextPartIndex] as { type: "text"; text: string }).text += delta;
|
||||||
|
|
@ -49,10 +35,7 @@ export function appendText(state: ContentPartsState, delta: string): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracted from page.tsx line 540 (onNew).
|
|
||||||
* Identical in handleResume (line 1029) and handleRegenerate (line 1407).
|
|
||||||
*/
|
|
||||||
export function addToolCall(
|
export function addToolCall(
|
||||||
state: ContentPartsState,
|
state: ContentPartsState,
|
||||||
toolsWithUI: Set<string>,
|
toolsWithUI: Set<string>,
|
||||||
|
|
@ -72,10 +55,7 @@ export function addToolCall(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracted from page.tsx line 540 (onNew).
|
|
||||||
* Identical in handleResume (line 1027) and handleRegenerate (line 1387).
|
|
||||||
*/
|
|
||||||
export function updateToolCall(
|
export function updateToolCall(
|
||||||
state: ContentPartsState,
|
state: ContentPartsState,
|
||||||
toolCallId: string,
|
toolCallId: string,
|
||||||
|
|
@ -89,10 +69,7 @@ export function updateToolCall(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracted from page.tsx line 539 (onNew).
|
|
||||||
* Identical in handleResume and handleRegenerate.
|
|
||||||
*/
|
|
||||||
export function buildContentForUI(
|
export function buildContentForUI(
|
||||||
state: ContentPartsState,
|
state: ContentPartsState,
|
||||||
toolsWithUI: Set<string>
|
toolsWithUI: Set<string>
|
||||||
|
|
@ -107,10 +84,7 @@ export function buildContentForUI(
|
||||||
: [{ type: "text", text: "" }];
|
: [{ type: "text", text: "" }];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracted from page.tsx line 553 (onNew).
|
|
||||||
* Identical in handleResume and handleRegenerate.
|
|
||||||
*/
|
|
||||||
export function buildContentForPersistence(
|
export function buildContentForPersistence(
|
||||||
state: ContentPartsState,
|
state: ContentPartsState,
|
||||||
toolsWithUI: Set<string>,
|
toolsWithUI: Set<string>,
|
||||||
|
|
@ -139,11 +113,7 @@ export function buildContentForPersistence(
|
||||||
/**
|
/**
|
||||||
* Async generator that reads an SSE stream and yields parsed JSON objects.
|
* Async generator that reads an SSE stream and yields parsed JSON objects.
|
||||||
* Handles buffering, event splitting, and skips malformed JSON / [DONE] lines.
|
* Handles buffering, event splitting, and skips malformed JSON / [DONE] lines.
|
||||||
*
|
|
||||||
* Extracted from the identical SSE reading boilerplate in onNew, handleResume,
|
|
||||||
* and handleRegenerate.
|
|
||||||
*/
|
*/
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: matches JSON.parse return type
|
|
||||||
export async function* readSSEStream(response: Response): AsyncGenerator<any> {
|
export async function* readSSEStream(response: Response): AsyncGenerator<any> {
|
||||||
if (!response.body) {
|
if (!response.body) {
|
||||||
throw new Error("No response body");
|
throw new Error("No response body");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue