审查视图

app/components/bgRemoval/bg-removal-panel.tsx 16.3 KB
202304001 authored
1 2 3
import { ControlParamItem } from "../sd";
import { IconButton } from "../button";
import styles from "./bg-removal-panel.module.scss";
202304001 authored
4 5
import { useState } from "react";
import { message } from "antd";
6
import type { LocalData, PanelProps } from "@/app/types/zuotang";
202304001 authored
7
import { ApiPath, bgremovalModel, Path } from "@/app/constant";
202304001 authored
8 9
import { useAccessStore } from "@/app/store";
import Locale from "@/app/locales";
202304001 authored
10
import LoadingIcon from "@/app/icons/three-dots.svg";
202304001 authored
11
import WriteIcon from "@/app/icons/write.svg";
202304001 authored
12
import { useChatStore } from "@/app/store";
202304001 authored
13
import { getBgPrompt } from "@/app/utils/prompt";
14 15
import { cosUploadImage } from "@/app/utils/tencentCos";
import { getFileByUrl } from "@/app/utils/fileUtil";
202304001 authored
16 17
// 错误消息映射函数
const getErrorMessage = (state: number): string => {
202304001 authored
18 19 20 21 22 23 24 25 26
  const errorMap: { [key: number]: string } = {
    [-8]: "处理超时,最长处理时间30秒",
    [-7]: "无效图片文件(可能已损坏或格式错误)",
    [-5]: "图片大小超过15MB限制",
    [-3]: "服务器下载图片失败,请检查URL有效性",
    [-2]: "处理结果上传失败",
    [-1]: "任务处理失败",
  };
  return errorMap[state] || `未知错误(状态码:${state})`;
202304001 authored
27 28 29 30
};

// 图片URL转Blob方法
const urlToBlob = async (url: string): Promise<Blob> => {
202304001 authored
31 32
  const response = await fetch(url);
  if (!response.ok) throw new Error(Locale.BgRemoval.error.imgLoadingErr);
33 34
  const blob = await response.blob();
  return blob;
202304001 authored
35 36 37 38
};

// 通用轮询函数
const useTaskPoller = () => {
202304001 authored
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
  const pollTask = async (
    taskId: string,
    endpoint: string,
    onSuccess: (data: any) => void,
    onError: (message: string) => void,
  ) => {
    const pollInterval = 1000;
    const maxAttempts = 60;
    let attempts = 0;
    const intervalId = setInterval(async () => {
      try {
        attempts++;
        if (attempts > maxAttempts) {
          clearInterval(intervalId);
          onError(Locale.BgRemoval.error.timeoutErr);
          return;
        }
        const result = await fetch(`${ApiPath.ZuoTang}/${endpoint}/${taskId}`);
        if (!result.ok) {
          const errorData = await result.json();
          throw new Error(errorData.message || Locale.BgRemoval.error.statuErr);
        }
        const taskResult = await result.json();
        // 根据 state 字段处理状态
        switch (taskResult.data?.state) {
          case 1: // 任务成功
            clearInterval(intervalId);
            onSuccess(taskResult.data);
            break;
          case 0: // 队列中
          case 2: // 准备中
          case 3: // 等待中
          case 4: // 处理中
            // 保持轮询不做操作
            break;
          default: // 处理错误状态
            clearInterval(intervalId);
            if (taskResult.data?.state < 0) {
              // 所有负数状态均为错误
              onError(getErrorMessage(taskResult.data.state));
            } else {
              onError("未知任务状态");
202304001 authored
81
            }
202304001 authored
82 83 84 85 86 87 88 89 90 91
        }
      } catch (error) {
        clearInterval(intervalId);
        onError(
          error instanceof Error
            ? error.message
            : Locale.BgRemoval.error.reqErr,
        );
      }
    }, pollInterval);
202304001 authored
92
202304001 authored
93 94
    return () => clearInterval(intervalId);
  };
202304001 authored
95
202304001 authored
96
  return { pollTask };
202304001 authored
97 98 99
};

// 日期处理工具
202304001 authored
100
export const useDateUtils = () => {
202304001 authored
101 102 103 104
  const getFormattedToday = (): string => {
    const today = new Date();
    return today.toISOString().split("T")[0].replace(/-/g, "");
  };
202304001 authored
105
202304001 authored
106 107 108
  const isToday = (dateStr: string): boolean => {
    return dateStr === getFormattedToday();
  };
202304001 authored
109
202304001 authored
110
  return { getFormattedToday, isToday };
202304001 authored
111 112 113
};

// 本地存储管理
202304001 authored
114
export const useLocalStorage = () => {
202304001 authored
115 116 117 118 119
  const { getFormattedToday, isToday } = useDateUtils();
  const getLocalData = (accessCode: string): LocalData => {
    try {
      const data = localStorage.getItem(accessCode);
      if (!data) return defaultLocalData();
202304001 authored
120
202304001 authored
121 122
      const parsed = JSON.parse(data);
      if (!isToday(parsed.date)) return defaultLocalData();
202304001 authored
123
202304001 authored
124 125 126
      return {
        date: parsed.date || getFormattedToday(),
        maxDailyUses: parsed.maxDailyUses || "first",
202304001 authored
127
        pptMaxUses: parsed.pptMsxUses || "first",
202304001 authored
128 129 130 131 132
      };
    } catch (e) {
      return defaultLocalData();
    }
  };
202304001 authored
133
202304001 authored
134 135 136 137 138 139 140 141 142 143
  const updateLocalUsage = (
    accessCode: string,
    maxDailyUses?: number,
    pptMaxUses?: number,
  ) => {
    const existingData = getLocalData(accessCode);
    // 检查现有数据的日期是否是今天
    const isNewDay = !isToday(existingData.date);
    // 创建新的数据对象
    const newData: LocalData = {
202304001 authored
144
      date: getFormattedToday(),
202304001 authored
145 146 147 148 149 150 151 152 153 154 155 156
      maxDailyUses:
        maxDailyUses !== undefined
          ? maxDailyUses.toString()
          : isNewDay
          ? "first"
          : existingData.maxDailyUses,
      pptMaxUses:
        pptMaxUses !== undefined
          ? pptMaxUses.toString()
          : isNewDay
          ? "first"
          : existingData.pptMaxUses,
202304001 authored
157
    };
202304001 authored
158
    localStorage.setItem(accessCode, JSON.stringify(newData));
202304001 authored
159
  };
202304001 authored
160
202304001 authored
161 162 163
  const defaultLocalData = (): LocalData => ({
    date: getFormattedToday(),
    maxDailyUses: "first",
202304001 authored
164
    pptMaxUses: "first",
202304001 authored
165 166
  });
  return { getLocalData, updateLocalUsage };
202304001 authored
167 168 169 170
};

// 定义组件的 props 类型
interface PromptListProps {
202304001 authored
171 172 173
  promptList: string[];
  setPromptList: React.Dispatch<React.SetStateAction<string[]>>;
  setPrompt: React.Dispatch<React.SetStateAction<string>>;
202304001 authored
174 175
}
const PromptListComponent: React.FC<PromptListProps> = ({
202304001 authored
176 177 178
  promptList,
  setPromptList,
  setPrompt,
202304001 authored
179
}) => {
202304001 authored
180 181 182 183 184
  // 删除处理函数
  const handleDelete = (index: number, e: React.MouseEvent) => {
    e.stopPropagation(); // 阻止冒泡
    setPromptList((prev) => prev.filter((_, i) => i !== index));
  };
202304001 authored
185
202304001 authored
186 187 188 189 190 191
  // 响应式样式配置
  const containerStyle: React.CSSProperties = {
    maxWidth: "90%",
    margin: "0 auto",
    padding: "0.6rem",
  };
202304001 authored
192
202304001 authored
193 194 195 196 197 198 199 200 201 202
  const itemStyle: React.CSSProperties = {
    position: "relative",
    backgroundColor: "#f8f9fa",
    padding: "0.5rem",
    margin: "0.5rem 0",
    borderRadius: "12px",
    cursor: "pointer",
    boxShadow: "0 2px 8px rgba(0,0,0,0.1)",
    transition: "all 0.3s ease",
  };
202304001 authored
203
202304001 authored
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219
  const deleteButtonStyle: React.CSSProperties = {
    position: "absolute",
    top: "0.2rem",
    right: "0.5rem",
    width: "15px",
    height: "15px",
    borderRadius: "50%",
    backgroundColor: "#ff4444",
    color: "white",
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    cursor: "pointer",
    transition: "all 0.2s ease",
    visibility: "hidden", // 默认隐藏
  };
202304001 authored
220
202304001 authored
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264
  return (
    <div style={containerStyle}>
      {promptList.map((item, index) => (
        <div
          key={index}
          style={itemStyle}
          onMouseEnter={(e) => {
            const currentTarget = e.currentTarget as HTMLElement;
            currentTarget.style.transform = "translateY(-3px)";
            currentTarget.style.boxShadow = "0 4px 12px rgba(0,0,0,0.15)";
            // 显示删除按钮
            const button = currentTarget.querySelector(".delete-btn");
            if (button) {
              (button as HTMLElement).style.visibility = "visible";
            }
          }}
          onMouseLeave={(e) => {
            const currentTarget = e.currentTarget as HTMLElement;
            currentTarget.style.transform = "translateY(0)";
            currentTarget.style.boxShadow = "0 2px 8px rgba(0,0,0,0.1)";
            // 隐藏删除按钮
            const button = currentTarget.querySelector(".delete-btn");
            if (button) {
              (button as HTMLElement).style.visibility = "hidden";
            }
          }}
          onClick={() => setPrompt(item)}
        >
          {/* 删除按钮 */}
          <div
            className="delete-btn"
            style={deleteButtonStyle}
            onClick={(e) => handleDelete(index, e)}
            onMouseEnter={(e) => {
              e.currentTarget.style.backgroundColor = "#cc0000";
              e.currentTarget.style.transform = "scale(1.1)";
            }}
            onMouseLeave={(e) => {
              e.currentTarget.style.backgroundColor = "#ff4444";
              e.currentTarget.style.transform = "scale(1)";
            }}
          >
            ×
          </div>
202304001 authored
265
202304001 authored
266 267 268 269 270 271 272 273 274 275 276
          {/* 内容区域 */}
          <div
            style={{
              fontSize: "clamp(12px, 2vw, 14px)",
              color: "#343a40",
              lineHeight: 1.6,
              paddingRight: "2rem", // 为删除按钮留出空间
            }}
          >
            {item}
          </div>
202304001 authored
277
        </div>
202304001 authored
278 279 280
      ))}
    </div>
  );
202304001 authored
281 282
};
283
export function BgPanel(props: PanelProps) {
202304001 authored
284 285 286 287 288 289
  const [prompt, setPrompt] = useState(""); //背景提示词
  const [promptList, setPromptList] = useState<string[]>([]); //背景提示词优化列表
  const [isGenerate, setIsGenerate] = useState(false);
  const [sceneTypeList, setSceneTypeList] = useState([]);
  const chatStore = useChatStore();
  const [loading, setLoading] = useState(false); //是否优化文案
290
  const { previewUrl, setPreviewUrl, isLoading, setIsLoading } = props;
202304001 authored
291 292 293
  const accessStore = useAccessStore();
  const { pollTask } = useTaskPoller();
  const { updateLocalUsage, getLocalData } = useLocalStorage();
202304001 authored
294
295 296 297 298 299
  const handleGenerateImg = async () => {
    if (!prompt.trim()) {
      return message.error("请先输入提示词");
    }
    setIsLoading(true);
202304001 authored
300 301 302 303 304 305 306 307 308 309
    try {
      const res = await fetch(`${ApiPath.OpenAiImg}/generations`, {
        method: "POST",
        body: JSON.stringify(prompt),
      });
      if (!res.ok) {
        const errorData = await res.json();
        throw new Error(errorData.message || Locale.BgRemoval.error.reqErr);
      }
      const responseData = await res.json();
310 311 312 313 314 315 316 317 318 319 320 321 322 323 324
      setPreviewUrl(responseData.data.data[0].url);
      //异步上传保存图片
      setTimeout(async () => {
        try {
          const file: File = await getFileByUrl(responseData.data.data[0].url);
          const imageUrl = await cosUploadImage(file, Path.BgRemoval);
          setPreviewUrl(
            imageUrl.startsWith("https://") ? imageUrl : `https://${imageUrl}`,
          );
        } catch (error) {
          console.log(error);

          message.error(Locale.ComError.UploadErr);
        }
      }, 0);
202304001 authored
325 326 327 328
    } catch (error) {
      message.error(Locale.BgRemoval.error.reqErr);
    } finally {
      setIsLoading(false);
329 330 331
    }
  };
202304001 authored
332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349
  const handleApiRequest = async (endpoint: string) => {
    if (!previewUrl) {
      message.error(Locale.BgRemoval.error.selectImg);
      throw new Error(`VALIDATION_ERROR: ${Locale.BgRemoval.error.selectImg}`);
    }
    if (!accessStore.accessCode) {
      message.error(Locale.BgRemoval.error.code);
      throw new Error(`VALIDATION_ERROR: ${Locale.BgRemoval.error.code}`);
    }
    if (endpoint === "visual/r-background" && !prompt.trim()) {
      message.error(Locale.BgRemoval.error.prompt);
      setIsLoading(false);
      throw new Error(`VALIDATION_ERROR: ${Locale.BgRemoval.error.prompt}`);
    }
    try {
      const formData = new FormData();
      const localData = getLocalData(accessStore.accessCode); // 获取本地数据
      formData.append("accessCode", accessStore.accessCode);
202304001 authored
350
      formData.append("localData", JSON.stringify(localData));
351
      formData.append("image_url", previewUrl);
202304001 authored
352 353 354 355 356 357 358 359 360 361 362 363 364 365 366
      if (endpoint === "visual/r-background") {
        formData.append("prompt", prompt);
      } //生成背景添加提示词
      const res = await fetch(`${ApiPath.ZuoTang}/${endpoint}`, {
        method: "POST",
        body: formData,
      });
      if (!res.ok) {
        const errorData = await res.json();
        throw new Error(errorData.message || Locale.BgRemoval.error.reqErr);
      }
      const responseData = await res.json();
      if (responseData.status >= 400) {
        if (responseData.status === 429) {
          updateLocalUsage(accessStore.accessCode, 0);
202304001 authored
367
        }
202304001 authored
368 369 370 371 372 373
        throw new Error(responseData.message);
      }
      return responseData;
    } finally {
    }
  };
202304001 authored
374
202304001 authored
375 376 377 378 379
  const handleProcessImage = async (endpoint: string) => {
    setIsLoading(true);
    try {
      const responseData = await handleApiRequest(endpoint);
      updateLocalUsage(accessStore.accessCode, responseData.maxDailyUses);
202304001 authored
380
202304001 authored
381 382 383 384 385
      pollTask(
        responseData.data.task_id,
        endpoint,
        async (data) => {
          try {
386
            setPreviewUrl(data.image || data.image_1);
202304001 authored
387
            message.success(Locale.BgRemoval.success);
388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404
            //异步上传保存图片
            setTimeout(async () => {
              try {
                const file: File = await getFileByUrl(
                  data.image || data.image_1,
                );
                const imageUrl = await cosUploadImage(file, Path.BgRemoval);
                setPreviewUrl(
                  imageUrl.startsWith("https://")
                    ? imageUrl
                    : `https://${imageUrl}`,
                );
              } catch (error) {
                console.log(error);
                message.error(Locale.ComError.UploadErr);
              }
            }, 0);
202304001 authored
405 406 407
          } catch (error) {
            message.error(Locale.BgRemoval.error.resultErr);
          } finally {
202304001 authored
408
            setIsLoading(false);
202304001 authored
409 410 411 412 413 414 415 416 417 418 419 420 421 422 423
          }
        },
        (errorMsg) => {
          message.error(errorMsg);
          setIsLoading(false);
        },
      );
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : "";
      if (!errorMessage.startsWith("VALIDATION_ERROR:")) {
        message.error(Locale.BgRemoval.error.reqErr);
      }
      setIsLoading(false);
    }
  };
202304001 authored
424
202304001 authored
425 426 427 428 429 430 431 432 433
  const handleDownload = () => {
    if (!previewUrl) return message.error(Locale.BgRemoval.error.downLoadErr);
    const link = document.createElement("a");
    link.href = previewUrl;
    link.download = `processed-${Date.now()}.png`;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
  };
202304001 authored
434
202304001 authored
435 436 437 438 439 440 441
  const optimizePrompt = async () => {
    try {
      if (!prompt.trim()) {
        return message.error(Locale.BgRemoval.error.prompt);
      }
      const input = getBgPrompt(prompt);
      setLoading(true);
202304001 authored
442
      const response = await chatStore.directLlmInvoke(input, bgremovalModel);
202304001 authored
443 444 445 446 447 448
      const items = response.split("'").filter((item) => item.trim() !== "");
      setPromptList(items);
    } catch (error) {
      message.error("优化失败,请重试");
    } finally {
      setLoading(false);
202304001 authored
449
    }
202304001 authored
450
  };
451
202304001 authored
452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490
  return (
    <>
      <ControlParamItem title={Locale.BgRemoval.promptTitle} required={true}>
        <div className={styles["prompt"]}>
          <textarea
            rows={3}
            className={styles["tx"]}
            placeholder={Locale.BgRemoval.error.prompt}
            onChange={(e) => {
              setPrompt(e.target.value);
            }}
            value={prompt}
          ></textarea>
          <div
            className={styles["ai-prompt"]}
            onClick={(e) => {
              if (loading) {
                e.stopPropagation();
                return;
              }
              optimizePrompt();
            }}
          >
            <WriteIcon />
            AI优化文案
          </div>
        </div>
        {loading ? (
          <div className={styles["loading-content"]}>
            <LoadingIcon />
          </div>
        ) : (
          <PromptListComponent
            promptList={promptList}
            setPromptList={setPromptList}
            setPrompt={setPrompt}
          />
        )}
      </ControlParamItem>
202304001 authored
491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522
      <ControlParamItem title={Locale.BgRemoval.subTitle}>
        <div className={styles["ai-models"]}>
          <IconButton
            text={Locale.BgRemoval.generateImg}
            type="primary"
            shadow
            onClick={handleGenerateImg}
            disabled={isLoading}
          />
          <IconButton
            text={Locale.BgRemoval.bgRemoveBtn}
            type="primary"
            shadow
            onClick={() => handleProcessImage("visual/segmentation")}
            disabled={isLoading}
          />
          <IconButton
            text={Locale.BgRemoval.downloadImg}
            type="primary"
            shadow
            onClick={handleDownload}
            disabled={!previewUrl}
          />
          <IconButton
            text={Locale.BgRemoval.generateBg}
            type="primary"
            shadow
            onClick={() => handleProcessImage("visual/r-background")}
            disabled={isLoading}
          />
        </div>
      </ControlParamItem>
202304001 authored
523 524 525
    </>
  );
}