diff --git a/orchestrator/src/client/api/client.ts b/orchestrator/src/client/api/client.ts index fd81062..6977ee5 100644 --- a/orchestrator/src/client/api/client.ts +++ b/orchestrator/src/client/api/client.ts @@ -9,6 +9,7 @@ import type { ApplicationTask, AppSettings, BackupInfo, + BranchInfo, DemoInfoResponse, Job, JobActionRequest, @@ -559,7 +560,7 @@ export async function listJobChatThreads(jobId: string): Promise<{ export async function listJobGhostwriterMessages( jobId: string, options?: { limit?: number; offset?: number }, -): Promise<{ messages: JobChatMessage[] }> { +): Promise<{ messages: JobChatMessage[]; branches: BranchInfo[] }> { const params = new URLSearchParams(); if (typeof options?.limit === "number") { params.set("limit", String(options.limit)); @@ -568,7 +569,7 @@ export async function listJobGhostwriterMessages( params.set("offset", String(options.offset)); } const query = params.toString(); - return fetchApi<{ messages: JobChatMessage[] }>( + return fetchApi<{ messages: JobChatMessage[]; branches: BranchInfo[] }>( `/jobs/${jobId}/chat/messages${query ? `?${query}` : ""}`, ); } @@ -747,6 +748,37 @@ export async function streamRegenerateJobGhostwriterMessage( ); } +export async function editJobGhostwriterMessage( + jobId: string, + messageId: string, + input: { content: string; signal?: AbortSignal }, + handlers: { + onEvent: (event: JobChatStreamEvent) => void; + }, +): Promise { + return streamSseEvents( + `/jobs/${jobId}/chat/messages/${messageId}/edit`, + { content: input.content, stream: true }, + { + onEvent: handlers.onEvent, + signal: input.signal, + }, + ); +} + +export async function switchJobGhostwriterBranch( + jobId: string, + messageId: string, +): Promise<{ messages: JobChatMessage[]; branches: BranchInfo[] }> { + return fetchApi<{ messages: JobChatMessage[]; branches: BranchInfo[] }>( + `/jobs/${jobId}/chat/messages/${messageId}/switch-branch`, + { + method: "POST", + body: JSON.stringify({}), + }, + ); +} + function toJobIdList(idOrIds: string | string[]): string[] { return Array.isArray(idOrIds) ? idOrIds : [idOrIds]; } diff --git a/orchestrator/src/client/components/ghostwriter/BranchNavigator.tsx b/orchestrator/src/client/components/ghostwriter/BranchNavigator.tsx new file mode 100644 index 0000000..8127b68 --- /dev/null +++ b/orchestrator/src/client/components/ghostwriter/BranchNavigator.tsx @@ -0,0 +1,44 @@ +import type { BranchInfo } from "@shared/types"; +import { ChevronLeft, ChevronRight } from "lucide-react"; +import type React from "react"; + +type BranchNavigatorProps = { + branchInfo: BranchInfo; + onSwitch: (messageId: string) => void; +}; + +export const BranchNavigator: React.FC = ({ + branchInfo, + onSwitch, +}) => { + const { siblingIds, activeIndex } = branchInfo; + const total = siblingIds.length; + const canGoLeft = activeIndex > 0; + const canGoRight = activeIndex < total - 1; + + return ( +
+ + + {activeIndex + 1}/{total} + + +
+ ); +}; diff --git a/orchestrator/src/client/components/ghostwriter/Composer.tsx b/orchestrator/src/client/components/ghostwriter/Composer.tsx index f5cf0d1..913a5fb 100644 --- a/orchestrator/src/client/components/ghostwriter/Composer.tsx +++ b/orchestrator/src/client/components/ghostwriter/Composer.tsx @@ -1,5 +1,5 @@ import { getMetaShortcutLabel, isMetaKeyPressed } from "@client/lib/meta-key"; -import { Eraser, RefreshCcw, Send, Square } from "lucide-react"; +import { Eraser, Send, Square } from "lucide-react"; import type React from "react"; import { useState } from "react"; import { Button } from "@/components/ui/button"; @@ -8,9 +8,7 @@ import { Textarea } from "@/components/ui/textarea"; type ComposerProps = { disabled?: boolean; isStreaming: boolean; - canRegenerate: boolean; canReset: boolean; - onRegenerate: () => Promise; onStop: () => Promise; onSend: (content: string) => Promise; onReset: () => void; @@ -19,9 +17,7 @@ type ComposerProps = { export const Composer: React.FC = ({ disabled, isStreaming, - canRegenerate, canReset, - onRegenerate, onStop, onSend, onReset, @@ -67,7 +63,7 @@ export const Composer: React.FC = ({ - {isStreaming ? ( + {isStreaming && ( - ) : ( - )} + )} + {!isUser && !isStreaming && !isActiveStreaming && ( + + )} + - {isActiveStreaming ? ( + + {isEditing ? ( +
+