feat: add EditorGridSizePicker and EditorSelectionFrame components; implement FreeCreateView for article management; introduce ArticleSelectionOptimizeService for optimizing article selections

This commit is contained in:
2026-04-06 22:18:55 +08:00
parent 08ace0a9e0
commit 1e844704e4
20 changed files with 3033 additions and 740 deletions
+87 -14
View File
@@ -5,6 +5,7 @@ import type {
ArticleImageUploadResponse,
ArticleListParams,
ArticleListResponse,
CreateArticleRequest,
ArticleVersion,
AuthTokens,
Brand,
@@ -210,6 +211,9 @@ export const articlesApi = {
list(params: ArticleListParams) {
return apiClient.get<ArticleListResponse>("/api/tenant/articles", { params });
},
create(payload: CreateArticleRequest = {}) {
return apiClient.post<ArticleDetail, CreateArticleRequest>("/api/tenant/articles", payload);
},
detail(id: number) {
return apiClient.get<ArticleDetail>(`/api/tenant/articles/${id}`);
},
@@ -305,6 +309,24 @@ export interface ArticleGenerationStreamEvent {
updated_at?: string;
}
export interface ArticleSelectionOptimizeRequest {
title: string;
markdown_content: string;
selected_text: string;
instruction: string;
}
export interface ArticleSelectionOptimizeStreamEvent {
type: string;
article_id: number;
status?: string;
delta?: string;
content?: string;
error?: string;
model?: string;
done?: boolean;
}
export function isGenerationStreamEnabled(): boolean {
return generationStreamEnabled;
}
@@ -316,18 +338,65 @@ export async function subscribeArticleGeneration(
onClose?: () => void;
},
signal: AbortSignal,
): Promise<void> {
return subscribeSSE<ArticleGenerationStreamEvent>(
`/api/tenant/articles/${articleId}/stream`,
{
method: "GET",
},
handlers,
signal,
);
}
export async function streamArticleSelectionOptimize(
articleId: number,
payload: ArticleSelectionOptimizeRequest,
handlers: {
onEvent: (event: ArticleSelectionOptimizeStreamEvent) => void;
onClose?: () => void;
},
signal: AbortSignal,
): Promise<void> {
return subscribeSSE<ArticleSelectionOptimizeStreamEvent>(
`/api/tenant/articles/${articleId}/selection-optimize/stream`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
},
handlers,
signal,
);
}
type SSEEventShape = {
type: string;
};
async function subscribeSSE<TEvent extends SSEEventShape>(
path: string,
init: RequestInit,
handlers: {
onEvent: (event: TEvent) => void;
onClose?: () => void;
},
signal: AbortSignal,
): Promise<void> {
const accessToken = getStoredAccessToken();
if (!accessToken) {
throw new Error("missing access token");
}
const response = await fetch(`${baseURL}/api/tenant/articles/${articleId}/stream`, {
method: "GET",
headers: {
Accept: "text/event-stream",
Authorization: `Bearer ${accessToken}`,
},
const requestHeaders = new Headers(init.headers);
requestHeaders.set("Accept", "text/event-stream");
requestHeaders.set("Authorization", `Bearer ${accessToken}`);
const response = await fetch(`${baseURL}${path}`, {
...init,
headers: requestHeaders,
signal,
});
@@ -350,11 +419,11 @@ export async function subscribeArticleGeneration(
}
buffer += decoder.decode(value, { stream: true });
buffer = consumeSSEBuffer(buffer, handlers.onEvent);
buffer = consumeSSEBuffer<TEvent>(buffer, handlers.onEvent);
}
buffer += decoder.decode();
consumeSSEBuffer(buffer, handlers.onEvent);
consumeSSEBuffer<TEvent>(buffer, handlers.onEvent);
} finally {
reader.releaseLock();
handlers.onClose?.();
@@ -543,9 +612,9 @@ export function normalizeInputParams(
};
}
function consumeSSEBuffer(
function consumeSSEBuffer<TEvent extends SSEEventShape>(
buffer: string,
onEvent: (event: ArticleGenerationStreamEvent) => void,
onEvent: (event: TEvent) => void,
): string {
let next = buffer;
@@ -558,7 +627,7 @@ function consumeSSEBuffer(
const rawEvent = next.slice(0, delimiterIndex);
next = next.slice(delimiterIndex + 2);
const parsed = parseSSEEvent(rawEvent);
const parsed = parseSSEEvent<TEvent>(rawEvent);
if (parsed) {
onEvent(parsed);
}
@@ -567,7 +636,7 @@ function consumeSSEBuffer(
return next;
}
function parseSSEEvent(raw: string): ArticleGenerationStreamEvent | null {
function parseSSEEvent<TEvent extends SSEEventShape>(raw: string): TEvent | null {
const lines = raw.split(/\r?\n/);
let event = "message";
const dataLines: string[] = [];
@@ -586,9 +655,13 @@ function parseSSEEvent(raw: string): ArticleGenerationStreamEvent | null {
return null;
}
const data = JSON.parse(dataLines.join("\n")) as unknown as ArticleGenerationStreamEvent;
const data = JSON.parse(dataLines.join("\n")) as Record<string, unknown> | null;
if (!data || Array.isArray(data)) {
return null;
}
return {
...data,
type: event,
};
} as TEvent;
}
+3
View File
@@ -54,6 +54,9 @@ export function getSourceTypeLabel(sourceType?: string | null, generationMode?:
if (sourceType === "template") {
return i18n.global.t("status.sourceType.template");
}
if (sourceType === "free_create") {
return i18n.global.t("status.sourceType.free_create");
}
if (sourceType === "custom_generation") {
if (generationMode === "schedule") {
return i18n.global.t("status.sourceType.schedule_task");