Browse Source

去掉侧边栏显示 接口去重

feature/add_provider_popi
TianYun 2 months ago
parent
commit
acebce01e4
  1. 79
      src/components/__tests__/GenerationComposer.test.tsx
  2. 62
      src/components/composer/GenerationComposer.tsx
  3. 3
      src/components/nodes/ControlPanel.tsx
  4. 90
      src/hooks/usePopiserverTaskPrice.ts

79
src/components/__tests__/GenerationComposer.test.tsx

@ -2,6 +2,7 @@
import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import type { ReactNode } from "react";
import { GenerationComposer } from "@/components/composer/GenerationComposer";
import { clearPopiserverTaskPriceCacheForTests } from "@/hooks/usePopiserverTaskPrice";
import { useWorkflowStore } from "@/store/workflowStore";
import type {
EaseCurveNodeData,
@ -321,6 +322,7 @@ function videoEdge(source: string, target: string, index: number, targetHandle =
describe("GenerationComposer", () => {
beforeEach(() => {
vi.clearAllMocks();
clearPopiserverTaskPriceCacheForTests();
useWorkflowStore.setState({
nodes: [],
edges: [],
@ -673,6 +675,7 @@ describe("GenerationComposer", () => {
nodes: [
imageNode("img-1", true, {
imageCount: 4,
parameterSchema: [],
selectedModel: {
provider: "popiserver",
modelId: "32",
@ -714,6 +717,71 @@ describe("GenerationComposer", () => {
});
});
it("reuses an in-flight PopiServer task price request across remounts", async () => {
let resolveTaskPrice: (response: Response) => void = () => undefined;
const taskPriceResponse = new Promise<Response>((resolve) => {
resolveTaskPrice = resolve;
});
const fetchMock = vi.fn((input: RequestInfo | URL) => {
const url = String(input);
if (url.startsWith("/api/points/calculate-task-price")) {
return taskPriceResponse;
}
if (url.startsWith("/api/models")) {
return Promise.resolve(new Response(JSON.stringify({ success: true, models: [] }), { status: 200 }));
}
return Promise.reject(new Error(`Unexpected fetch: ${url}`));
});
vi.stubGlobal("fetch", fetchMock);
useWorkflowStore.setState({
nodes: [
imageNode("img-1", true, {
imageCount: 4,
parameterSchema: [],
selectedModel: {
provider: "popiserver",
modelId: "32",
displayName: "Popi Image",
capabilities: ["text-to-image", "image-to-image"],
metadata: {
aiModelId: 32,
aiModelCode: "gpt-image-2-all",
aiModelCodeAlias: "gpt-image-2-all",
type: 1,
subType: 102,
billingMethod: 1,
},
},
}),
],
});
const firstRender = render(<GenerationComposer />);
await waitFor(() => {
expect(fetchMock).toHaveBeenCalledTimes(1);
});
firstRender.unmount();
render(<GenerationComposer />);
await waitFor(() => {
expect(screen.getByTestId("node-inline-composer")).toBeInTheDocument();
});
expect(fetchMock).toHaveBeenCalledTimes(1);
resolveTaskPrice(new Response(JSON.stringify({
success: true,
data: {
taskPrice: 36,
},
}), { status: 200 }));
await waitFor(() => {
expect(screen.getByRole("button", { name: "36 points" })).toBeInTheDocument();
});
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it("renders gateway image dimension options by schema name", () => {
useWorkflowStore.setState({
nodes: [
@ -830,6 +898,7 @@ describe("GenerationComposer", () => {
aiModelCodeAlias: "seedance-video-pro",
type: 2,
subType: 203,
billingMethod: 1,
},
},
}),
@ -856,7 +925,13 @@ describe("GenerationComposer", () => {
});
it("does not show PopiServer points when the model cannot be priced", async () => {
const fetchMock = vi.fn();
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
const url = String(input);
if (url.startsWith("/api/models")) {
return new Response(JSON.stringify({ success: true, models: [] }), { status: 200 });
}
throw new Error(`Unexpected fetch: ${url}`);
});
vi.stubGlobal("fetch", fetchMock);
useWorkflowStore.setState({
nodes: [
@ -874,7 +949,7 @@ describe("GenerationComposer", () => {
render(<GenerationComposer />);
expect(screen.queryByRole("button", { name: /points/ })).not.toBeInTheDocument();
expect(fetchMock).not.toHaveBeenCalled();
expect(fetchMock.mock.calls.some(([input]) => String(input).startsWith("/api/points/calculate-task-price"))).toBe(false);
});
it("hydrates NewApiWG point cost for stored models without pricing", async () => {

62
src/components/composer/GenerationComposer.tsx

@ -41,6 +41,7 @@ import { isPopiProviderMode } from "@/lib/providerMode";
import { ModelSearchDialog } from "@/components/modals/ModelSearchDialog";
import type { ModelParameter, ProviderModel } from "@/lib/providers/types";
import { useI18n } from "@/i18n";
import { usePopiserverTaskPrice } from "@/hooks/usePopiserverTaskPrice";
import { DEFAULT_NEWAPIWG_LLM_MODEL_ID, LLM_MODELS } from "@/lib/llmModels";
import {
DEFAULT_VIDEO_DURATION_SECONDS,
@ -99,13 +100,6 @@ type PointsEstimateResponse = {
};
};
type TaskPriceResponse = {
success?: boolean;
data?: {
taskPrice?: unknown;
};
};
type SeedanceVideoEstimatePayload = {
modelName: string;
estimationType: "seedance_video";
@ -503,11 +497,6 @@ function getEstimatedPointAmount(response: PointsEstimateResponse): number | nul
return finiteNumberFromUnknown(response.data?.estimated_points);
}
function getTaskPricePointAmount(response: TaskPriceResponse): number | null {
const amount = finiteNumberFromUnknown(response.data?.taskPrice);
return amount !== null && amount > 0 ? amount : null;
}
function getNodeVideoDurationSeconds(node: WorkflowNode | undefined): number {
if (!node) return 0;
const data = node.data as Record<string, unknown>;
@ -827,7 +816,6 @@ export function GenerationComposer() {
const [isComposerCollapsed, setIsComposerCollapsed] = useState(false);
const [referencePreviewIndex, setReferencePreviewIndex] = useState<number | null>(null);
const [seedanceEstimatedPointAmount, setSeedanceEstimatedPointAmount] = useState<number | null>(null);
const [popiserverEstimatedPointAmount, setPopiserverEstimatedPointAmount] = useState<number | null>(null);
const referenceImageInputRef = useRef<HTMLInputElement>(null);
const contextRef = useRef(context);
@ -835,7 +823,6 @@ export function GenerationComposer() {
const dirtyFieldsRef = useRef(dirtyFields);
const newApiWGPricingCacheRef = useRef<Map<string, SelectedModel["pricing"]>>(new Map());
const seedanceEstimateCacheRef = useRef<Map<string, number>>(new Map());
const popiserverTaskPriceCacheRef = useRef<Map<string, number>>(new Map());
useEffect(() => {
draftRef.current = draft;
@ -980,9 +967,7 @@ export function GenerationComposer() {
modelSchemaReadyForEstimate,
]);
const popiserverTaskPriceKey = popiserverTaskPricePayload
? JSON.stringify(popiserverTaskPricePayload)
: null;
const popiserverEstimatedPointAmount = usePopiserverTaskPrice(popiserverTaskPricePayload);
useEffect(() => {
const selectedModel = draft.selectedModel;
@ -1204,49 +1189,6 @@ export function GenerationComposer() {
};
}, [seedanceEstimateKey, seedanceEstimatePayload]);
useEffect(() => {
if (!popiserverTaskPricePayload || !popiserverTaskPriceKey) {
setPopiserverEstimatedPointAmount(null);
return;
}
const cachedAmount = popiserverTaskPriceCacheRef.current.get(popiserverTaskPriceKey);
if (cachedAmount !== undefined) {
setPopiserverEstimatedPointAmount(cachedAmount);
return;
}
let cancelled = false;
setPopiserverEstimatedPointAmount(null);
const loadTaskPrice = async () => {
try {
const response = await fetch("/api/points/calculate-task-price", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(popiserverTaskPricePayload),
});
if (!response.ok) return;
const data = (await response.json()) as TaskPriceResponse;
const taskPrice = getTaskPricePointAmount(data);
if (taskPrice === null) return;
popiserverTaskPriceCacheRef.current.set(popiserverTaskPriceKey, taskPrice);
if (!cancelled) setPopiserverEstimatedPointAmount(taskPrice);
} catch {
// Estimation failures should not block generation; the run request still performs the real charge.
}
};
void loadTaskPrice();
return () => {
cancelled = true;
};
}, [popiserverTaskPriceKey, popiserverTaskPricePayload]);
const isVideoStitchNode = context.mode === "process-node" && context.node?.type === "videoStitch";
const isEaseCurveNode = context.mode === "process-node" && context.node?.type === "easeCurve";
const isProcessNode = isVideoStitchNode || isEaseCurveNode;

3
src/components/nodes/ControlPanel.tsx

@ -35,10 +35,7 @@ import {
// List of node types that have configurable parameters
const CONFIGURABLE_NODE_TYPES: NodeType[] = [
"nanoBanana",
"generateVideo",
"generate3d",
"generateAudio",
"llmGenerate",
"easeCurve",
"conditionalSwitch",

90
src/hooks/usePopiserverTaskPrice.ts

@ -0,0 +1,90 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import type { PopiserverTaskPricePayload } from "@/utils/generationConfig";
type TaskPriceResponse = {
success?: boolean;
data?: {
taskPrice?: unknown;
};
};
const taskPriceCache = new Map<string, number>();
const taskPriceInflight = new Map<string, Promise<number | null>>();
function finiteNumberFromUnknown(value: unknown): number | null {
if (typeof value === "number" && Number.isFinite(value)) return value;
if (typeof value === "string") {
const parsed = Number.parseFloat(value.trim());
return Number.isFinite(parsed) ? parsed : null;
}
return null;
}
function getTaskPricePointAmount(response: TaskPriceResponse): number | null {
const amount = finiteNumberFromUnknown(response.data?.taskPrice);
return amount !== null && amount > 0 ? amount : null;
}
export function usePopiserverTaskPrice(payload: PopiserverTaskPricePayload | null): number | null {
const [pointAmount, setPointAmount] = useState<number | null>(null);
const body = useMemo(() => payload ? JSON.stringify(payload) : null, [payload]);
useEffect(() => {
if (!body) {
setPointAmount(null);
return;
}
const cachedAmount = taskPriceCache.get(body);
if (cachedAmount !== undefined) {
setPointAmount(cachedAmount);
return;
}
let cancelled = false;
setPointAmount(null);
let request = taskPriceInflight.get(body);
if (!request) {
request = fetch("/api/points/calculate-task-price", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body,
})
.then(async (response) => {
if (!response.ok) return null;
const data = (await response.json()) as TaskPriceResponse;
const taskPrice = getTaskPricePointAmount(data);
if (taskPrice !== null) {
taskPriceCache.set(body, taskPrice);
}
return taskPrice;
})
.catch(() => null)
.finally(() => {
taskPriceInflight.delete(body);
});
taskPriceInflight.set(body, request);
}
void request.then((taskPrice) => {
if (!cancelled) setPointAmount(taskPrice);
});
return () => {
cancelled = true;
};
}, [body]);
return pointAmount;
}
export function clearPopiserverTaskPriceCacheForTests() {
taskPriceCache.clear();
taskPriceInflight.clear();
}
Loading…
Cancel
Save