feat: add EditorGridSizePicker and EditorSelectionFrame components; implement FreeCreateView for article management; introduce ArticleSelectionOptimizeService for optimizing article selections
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user