Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions packages/app-shell/src/layout/ConsoleFloatingChatbot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
} from '@object-ui/components';
import { Share2, SquarePen } from 'lucide-react';
import { useObjectTranslation } from '@object-ui/i18n';
import { toast } from 'sonner';
import { useChatConversation, type HydratedUIMessage } from '../hooks';
import { useAssistant, requestAssistantReview, type AssistantEditorContext } from '../assistant/assistantBus';

Expand Down Expand Up @@ -93,6 +94,9 @@ function buildChatLocale(
newChat: '开启新对话',
share: '分享对话',
reviewDraft: (n: number) => `查看 ${n} 项变更`,
publishDrafts: '发布',
publishOk: '已发布,对象已生效。',
publishFailed: '发布失败',
suggestions,
};
}
Expand All @@ -118,6 +122,9 @@ function buildChatLocale(
newChat: 'New chat',
share: 'Share conversation',
reviewDraft: (n: number) => `Review ${n} change${n === 1 ? '' : 's'}`,
publishDrafts: 'Publish',
publishOk: 'Published — objects are now live.',
publishFailed: 'Publish failed',
suggestions,
};
}
Expand Down Expand Up @@ -447,6 +454,34 @@ function ChatbotInner({
if (items[0]) requestAssistantReview(items[0]);
}}
toolReviewLabel={locale.reviewDraft}
onPublishDrafts={async (packageId) => {
// ADR-0033 — promote the conversation's staged drafts to live in one
// click (the human still confirms here). Mirrors PackagesPage's
// publish-drafts call; cookie-authenticated like the rest of the SPA.
try {
const res = await fetch(
`/api/v1/packages/${encodeURIComponent(packageId)}/publish-drafts`,
{
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: '{}',
},
);
const payload = await res.json().catch(() => null);
if (!res.ok || payload?.success === false) {
throw new Error(payload?.error?.message || `HTTP ${res.status}`);
}
const failed = payload?.data?.failedCount ?? payload?.failedCount ?? 0;
if (failed) throw new Error(String(failed));
toast.success(locale.publishOk);
} catch (e) {
toast.error(locale.publishFailed, {
description: e instanceof Error ? e.message : undefined,
});
}
}}
publishDraftsLabel={locale.publishDrafts}
/>
{conversationId && (
<ShareDialog
Expand Down
55 changes: 44 additions & 11 deletions packages/plugin-chatbot/src/ChatbotEnhanced.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { cn } from '@object-ui/components';
import { AlertCircle, Copy, Check, RefreshCw, CornerDownLeft, Bot, GitCompareArrows } from 'lucide-react';
import { AlertCircle, Copy, Check, RefreshCw, CornerDownLeft, Bot, GitCompareArrows, Rocket } from 'lucide-react';
import type { ChatStatus } from 'ai';
import {
humanizeToolName,
Expand Down Expand Up @@ -134,7 +134,7 @@ export interface ChatToolInvocation {
* change(s)" affordance that opens the designer's review/diff. Nothing is
* live until the human publishes — this is the review entry point.
*/
draftReview?: { items: Array<{ type: string; name: string }>; summary?: string };
draftReview?: { items: Array<{ type: string; name: string }>; summary?: string; packageId?: string };
}

export interface ChatSource {
Expand Down Expand Up @@ -267,6 +267,16 @@ export interface ChatbotEnhancedProps extends React.HTMLAttributes<HTMLDivElemen
onReviewDraft?: (items: Array<{ type: string; name: string }>) => void;
/** Label for the review-draft button (default "Review {n} change(s)"). */
toolReviewLabel?: (count: number) => string;
/**
* When provided AND the drafted tool result reported its owning `packageId`,
* tool parts render a one-click "Publish" button so the human can promote the
* staged drafts to live without leaving the conversation (the ADR-0033 gate
* stays — the human still clicks). The host wires this to
* `POST /api/v1/packages/:packageId/publish-drafts`.
*/
onPublishDrafts?: (packageId: string) => void;
/** Label for the publish-drafts button (default "Publish"). */
publishDraftsLabel?: string;
}

export type ToolDecisionState =
Expand Down Expand Up @@ -344,6 +354,8 @@ const ChatbotEnhanced = React.forwardRef<HTMLDivElement, ChatbotEnhancedProps>(
toolDecisions,
onReviewDraft,
toolReviewLabel = (n) => `Review ${n} change${n === 1 ? '' : 's'}`,
onPublishDrafts,
publishDraftsLabel = 'Publish',
...props
},
ref
Expand Down Expand Up @@ -600,16 +612,37 @@ const ChatbotEnhanced = React.forwardRef<HTMLDivElement, ChatbotEnhancedProps>(
</button>
</div>
) : null}
{tool.draftReview && tool.draftReview.items.length > 0 && onReviewDraft ? (
{tool.draftReview &&
tool.draftReview.items.length > 0 &&
(onReviewDraft ||
(onPublishDrafts && tool.draftReview.packageId)) ? (
<div className="flex items-center gap-2 p-3 border-t bg-muted/30">
<button
type="button"
onClick={() => onReviewDraft(tool.draftReview!.items)}
className="inline-flex h-7 items-center gap-1.5 rounded-md bg-primary px-3 text-xs font-medium text-primary-foreground hover:bg-primary/90"
>
<GitCompareArrows className="size-3.5" />
{toolReviewLabel(tool.draftReview.items.length)}
</button>
{onPublishDrafts && tool.draftReview.packageId ? (
<button
type="button"
onClick={() =>
onPublishDrafts(tool.draftReview!.packageId!)
}
className="inline-flex h-7 items-center gap-1.5 rounded-md bg-primary px-3 text-xs font-medium text-primary-foreground hover:bg-primary/90"
>
<Rocket className="size-3.5" />
{publishDraftsLabel}
</button>
) : null}
{onReviewDraft ? (
<button
type="button"
onClick={() => onReviewDraft(tool.draftReview!.items)}
className={
onPublishDrafts && tool.draftReview.packageId
? 'inline-flex h-7 items-center gap-1.5 rounded-md border px-3 text-xs font-medium hover:bg-muted'
: 'inline-flex h-7 items-center gap-1.5 rounded-md bg-primary px-3 text-xs font-medium text-primary-foreground hover:bg-primary/90'
}
>
<GitCompareArrows className="size-3.5" />
{toolReviewLabel(tool.draftReview.items.length)}
</button>
) : null}
{tool.draftReview.summary ? (
<span className="truncate text-xs text-muted-foreground">
{tool.draftReview.summary}
Expand Down
12 changes: 10 additions & 2 deletions packages/plugin-chatbot/src/mapMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ function detectPendingApproval(
*/
function detectDraftResult(
result: unknown,
): { items: Array<{ type: string; name: string }>; summary?: string } | undefined {
): { items: Array<{ type: string; name: string }>; summary?: string; packageId?: string } | undefined {
const obj = parseResultEnvelope(result);
if (!obj || obj.status !== 'drafted') return undefined;
const items: Array<{ type: string; name: string }> = [];
Expand All @@ -143,7 +143,15 @@ function detectDraftResult(
items.push({ type: obj.type, name: obj.name });
}
if (items.length === 0) return undefined;
return { items, summary: typeof obj.summary === 'string' ? obj.summary : undefined };
// The owning package (when the staging tool reported it) lets the chat offer
// a one-click "publish" — POST /packages/:packageId/publish-drafts — so the
// ADR-0033 human gate is reachable from the conversation, not just a deep
// link into the designer.
return {
items,
summary: typeof obj.summary === 'string' ? obj.summary : undefined,
...(typeof obj.packageId === 'string' && obj.packageId ? { packageId: obj.packageId } : {}),
};
}

function extractToolInvocations(parts: AnyPart[]): ChatToolInvocation[] {
Expand Down
Loading