From 8a4eaa867d5c4f5d88b72efeb9d221ca413cd1ae Mon Sep 17 00:00:00 2001 From: 202304001 <941456317@qq.com> Date: Fri, 28 Mar 2025 19:26:59 +0800 Subject: [PATCH] 新增生成PPT功能,新增生成图片功能 --- app/api/[provider]/[...path]/route.ts | 11 +++++++++-- app/api/docmee.ts | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ app/api/generateImg.ts | 42 ++++++++++++++++++++++++++++++++++++++++++ app/api/openai.ts | 2 -- app/api/zuotang.ts | 229 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------------------------------------------------------------------------------------------- app/components/bgRemoval/bg-removal-panel.tsx | 26 ++++++++++++++++++++++++++ app/components/chat.tsx | 15 +++++++++++++++ app/components/home.tsx | 15 +++++++++++++-- app/components/mind/mind.tsx | 4 ++-- app/components/powerpoint/index.tsx | 1 + app/components/powerpoint/powerpoint.module.scss | 4 ++++ app/components/powerpoint/powerpoint.tsx | 168 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ app/components/powerpoint/ppt-siderbar.tsx | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ app/components/settings.tsx | 4 ++++ app/components/sidebar.tsx | 3 ++- app/config/server.ts | 18 +++++++++++++----- app/constant.ts | 9 ++++++--- app/locales/cn.ts | 48 ++++++++++++++++++++++++------------------------ app/types/docmee.d.ts | 24 ++++++++++++++++++++++++ package.json | 1 + yarn.lock | 31 +++++++++++++++++++++++++++++++ 21 files changed, 631 insertions(+), 154 deletions(-) create mode 100644 app/api/docmee.ts create mode 100644 app/api/generateImg.ts create mode 100644 app/components/powerpoint/index.tsx create mode 100644 app/components/powerpoint/powerpoint.module.scss create mode 100644 app/components/powerpoint/powerpoint.tsx create mode 100644 app/components/powerpoint/ppt-siderbar.tsx create mode 100644 app/types/docmee.d.ts diff --git a/app/api/[provider]/[...path]/route.ts b/app/api/[provider]/[...path]/route.ts index 3e14932..32aa190 100644 --- a/app/api/[provider]/[...path]/route.ts +++ b/app/api/[provider]/[...path]/route.ts @@ -16,7 +16,10 @@ import { handle as xaiHandler } from "../../xai"; import { handle as chatglmHandler } from "../../glm"; import { handle as proxyHandler } from "../../proxy"; //20250321 新增佐糖API -import {handle as zuotangHandler} from "../../zuotang"; +import { handle as zuotangHandler } from "../../zuotang"; +//20250328新增PPT API +import { handle as docmeeHandler } from "../../docmee"; +import { handle as generateImgHandler } from "../../generateImg"; async function handle( req: NextRequest, @@ -55,7 +58,11 @@ async function handle( case ApiPath.OpenAI: return openaiHandler(req, { params }); case ApiPath.ZuoTang: - return zuotangHandler(req,{ params }) + return zuotangHandler(req, { params }); + case ApiPath.Docmee: + return docmeeHandler(req, { params }); + case ApiPath.OpenAiImg: + return generateImgHandler(req, { params }); default: return proxyHandler(req, { params }); } diff --git a/app/api/docmee.ts b/app/api/docmee.ts new file mode 100644 index 0000000..3fabe28 --- /dev/null +++ b/app/api/docmee.ts @@ -0,0 +1,48 @@ +import { getServerSideConfig } from "@/app/config/server"; +import { NextRequest, NextResponse } from "next/server"; + +export async function handle( + req: NextRequest, + { params }: { params: { path: string[] } }, +) { + const config = getServerSideConfig(); + const baseUrl = config.docmeeUrl; + const subPath = params.path.join("/"); + // if(subPath==='createApiToken'){const reqUrl = `${baseUrl}/api/user/${subPath}`;} + const reqUrl = `${baseUrl}/api/user/${subPath}`; + console.log(reqUrl); + try { + // 获取 accessCode + const body = await new Response(req.body).text(); + const uid = JSON.parse(body); + const headers = new Headers({ + "Api-Key": config.docmeeApiKey, + "Content-Type": "application/json", + }); + console.log("********************" + config.docmeeApiKey); + // 使用 async/await 处理 fetch 请求 + const response = await fetch(reqUrl, { + headers, + method: "POST", + body: JSON.stringify({ uid: uid, limit: config.docmeeMaxDailyUses }), + }); + if (!response.ok) { + throw new Error( + `HTTP error! status: ${response.status} ${response.text()}`, + ); + } + const data = await response.json(); + console.log("-----------------data", data); + // 返回固定的 token + return NextResponse.json({ + data: data, + status: 200, + }); + } catch (error) { + console.error("处理请求时出错:", error); + return NextResponse.json({ + data: "处理请求时出错", + status: 500, + }); + } +} diff --git a/app/api/generateImg.ts b/app/api/generateImg.ts new file mode 100644 index 0000000..0b72425 --- /dev/null +++ b/app/api/generateImg.ts @@ -0,0 +1,42 @@ +import { getServerSideConfig } from "@/app/config/server"; +import { NextRequest, NextResponse } from "next/server"; + +export async function handle( + req: NextRequest, + { params }: { params: { path: string[] } }, +) { + const config = getServerSideConfig(); + const baseUrl = config.baseUrl; + const subPath = params.path.join("/"); + // if(subPath==='createApiToken'){const reqUrl = `${baseUrl}/api/user/${subPath}`;} + const reqUrl = `${baseUrl}/v1/images/${subPath}`; + const apiKey = config.apiKey; + try { + const headers = new Headers({ + Authorization: `Bearer ${apiKey}`, + "Content-Type": "application/json", + }); + const body = await new Response(req.body).text(); + const prompt = JSON.parse(body); + const response = await fetch(reqUrl, { + headers, + method: "POST", + body: JSON.stringify({ + model: "dall-e-3", + prompt: prompt, + n: 1, + size: "1024x1024", + }), + }); + const data = await response.json(); + return NextResponse.json({ + data: data, + status: 200, + }); + } catch { + return NextResponse.json({ + data: "处理请求时出错", + status: 500, + }); + } +} diff --git a/app/api/openai.ts b/app/api/openai.ts index b16b1ce..94bad5a 100644 --- a/app/api/openai.ts +++ b/app/api/openai.ts @@ -30,7 +30,6 @@ export async function handle( req: NextRequest, { params }: { params: { path: string[] } }, ) { - console.log("------------------------------------进入opanai handle") console.log("[OpenAI Route] params ", params); if (req.method === "OPTIONS") { @@ -61,7 +60,6 @@ export async function handle( try { const response = await requestOpenai(req); - // list models if (subpath === OpenaiPath.ListModelPath && response.status === 200) { const resJson = (await response.json()) as OpenAIListModelResponse; diff --git a/app/api/zuotang.ts b/app/api/zuotang.ts index b10b47b..2a8bb43 100644 --- a/app/api/zuotang.ts +++ b/app/api/zuotang.ts @@ -1,135 +1,138 @@ import { getServerSideConfig } from "@/app/config/server"; -import type { CreateTaskResponse, GetTaskResponse, GetGenerateTaskResponse, LocalData } from "../types/zuotang"; +import type { + CreateTaskResponse, + GetTaskResponse, + GetGenerateTaskResponse, + LocalData, +} from "../types/zuotang"; import { NextRequest, NextResponse } from "next/server"; -import { error } from "console"; -import { message } from "antd"; import md5 from "spark-md5"; - // 类型守卫函数 function isString(value: unknown): value is string { - return typeof value === "string"; + return typeof value === "string"; } // 统一错误响应生成器 function createErrorResponse( - message: string, - status: number, - maxDailyUses?: number + message: string, + status: number, + maxDailyUses?: number, ): NextResponse<CreateTaskResponse> { - const response: CreateTaskResponse = { - status, - message, - data: { task_id: "" }, - maxDailyUses, - }; - return NextResponse.json(response, { status, headers: { "Content-Type": "application/json" } }); + const response: CreateTaskResponse = { + status, + message, + data: { task_id: "" }, + maxDailyUses, + }; + return NextResponse.json(response, { + status, + headers: { "Content-Type": "application/json" }, + }); } // 处理每日使用限制逻辑 function parseDailyUsage(allowNum: string, configMax: number): number { - if (allowNum === "first") return configMax; - const parsed = parseInt(allowNum, 10); - return Number.isNaN(parsed) ? configMax : parsed; + if (allowNum === "first") return configMax; + const parsed = parseInt(allowNum, 10); + return Number.isNaN(parsed) ? configMax : parsed; } - export async function handle( - req: NextRequest, - { params }: { params: { path: string[] } } + req: NextRequest, + { params }: { params: { path: string[] } }, ) { - const config = getServerSideConfig(); - const baseUrl = config.bgRemovalUrl; - const subPath = params.path.join("/"); - const reqUrl = `${baseUrl}/api/tasks/${subPath}`; - - try { - if (req.method === "POST") { - const formData = await req.formData(); - - // 验证访问码 - const accessCode = formData.get("accessCode"); - if (!isString(accessCode) || !config.codes.has(md5.hash(accessCode))) { - return createErrorResponse("无效访问密码!", 401); - } - - // 解析使用限制数据 - const localData = formData.get("localData"); - if (!isString(localData)) { - return createErrorResponse("无效请求参数", 400); - } - - const localDataObj: LocalData = JSON.parse(localData); - const maxDailyUses = parseDailyUsage(localDataObj.maxDailyUses, config.maxDailyUses); - - if (maxDailyUses <= 0) { - return createErrorResponse("今日次数已用完!", 429, 0); - } - - // 准备API请求 - const imageFile = formData.get("image_file"); - if (!(imageFile instanceof Blob)) { - return createErrorResponse("无效图片文件", 400); - } - - const headers = new Headers({ "X-API-KEY": config.bgRemovalApiKey }); - const newFormData = new FormData(); - newFormData.append("image_file", imageFile); - - if (subPath === "visual/r-background") { - newFormData.append("batch_size", "1"); - const prompt = formData.get("prompt") as string; - const trimmedPrompt = prompt ? prompt.trim() : null; - if (!trimmedPrompt) { - return createErrorResponse("背景提示词不能为空!", 400); - } - newFormData.append("prompt", trimmedPrompt); - } - - const response = await fetch(reqUrl, { - headers, - method: "POST", - body: newFormData, - }); - - if (!response.ok) { - throw new Error(`API请求失败: ${response.statusText}`); - } - - const responseData: CreateTaskResponse = await response.json(); - responseData.maxDailyUses = maxDailyUses - 1; - - return NextResponse.json(responseData, { - status: response.status, - headers: { "Content-Type": "application/json" }, - }); - - } else if (req.method === "GET") { - const headers = { "X-API-KEY": config.bgRemovalApiKey }; - const response = await fetch(reqUrl, { headers }); - - if (!response.ok) { - throw new Error(`API请求失败: ${response.statusText}`); - } - - const isVisualRoute = subPath.includes("visual/r-background"); - const responseData = isVisualRoute - ? (await response.json() as GetTaskResponse) - : (await response.json() as GetGenerateTaskResponse); - - return NextResponse.json(responseData, { - status: response.status, - headers: { "Content-Type": "application/json" }, - }); + const config = getServerSideConfig(); + const baseUrl = config.bgRemovalUrl; + const subPath = params.path.join("/"); + const reqUrl = `${baseUrl}/api/tasks/${subPath}`; + + try { + if (req.method === "POST") { + const formData = await req.formData(); + + // 验证访问码 + const accessCode = formData.get("accessCode"); + if (!isString(accessCode) || !config.codes.has(md5.hash(accessCode))) { + return createErrorResponse("无效访问密码!", 401); + } + // 解析使用限制数据 + const localData = formData.get("localData"); + if (!isString(localData)) { + return createErrorResponse("无效请求参数", 400); + } + const localDataObj: LocalData = JSON.parse(localData); + const maxDailyUses = parseDailyUsage( + localDataObj.maxDailyUses, + config.maxDailyUses, + ); + + if (maxDailyUses <= 0) { + return createErrorResponse("今日次数已用完!", 429, 0); + } + + // 准备API请求 + const imageFile = formData.get("image_file"); + if (!(imageFile instanceof Blob)) { + return createErrorResponse("无效图片文件", 400); + } + + const headers = new Headers({ "X-API-KEY": config.bgRemovalApiKey }); + const newFormData = new FormData(); + newFormData.append("image_file", imageFile); + + if (subPath === "visual/r-background") { + newFormData.append("batch_size", "1"); + const prompt = formData.get("prompt") as string; + const trimmedPrompt = prompt ? prompt.trim() : null; + if (!trimmedPrompt) { + return createErrorResponse("背景提示词不能为空!", 400); } - - return createErrorResponse("方法不允许", 405); - - } catch (error) { - console.error("请求处理错误:", error); - return createErrorResponse( - error instanceof Error ? error.message : "服务器内部错误", - 500 - ); + newFormData.append("prompt", trimmedPrompt); + } + + const response = await fetch(reqUrl, { + headers, + method: "POST", + body: newFormData, + }); + + if (!response.ok) { + throw new Error(`API请求失败: ${response.statusText}`); + } + + const responseData: CreateTaskResponse = await response.json(); + responseData.maxDailyUses = maxDailyUses - 1; + + return NextResponse.json(responseData, { + status: response.status, + headers: { "Content-Type": "application/json" }, + }); + } else if (req.method === "GET") { + const headers = { "X-API-KEY": config.bgRemovalApiKey }; + const response = await fetch(reqUrl, { headers }); + + if (!response.ok) { + throw new Error(`API请求失败: ${response.statusText}`); + } + + const isVisualRoute = subPath.includes("visual/r-background"); + const responseData = isVisualRoute + ? ((await response.json()) as GetTaskResponse) + : ((await response.json()) as GetGenerateTaskResponse); + + return NextResponse.json(responseData, { + status: response.status, + headers: { "Content-Type": "application/json" }, + }); } + + return createErrorResponse("方法不允许", 405); + } catch (error) { + console.error("请求处理错误:", error); + return createErrorResponse( + error instanceof Error ? error.message : "服务器内部错误", + 500, + ); + } } diff --git a/app/components/bgRemoval/bg-removal-panel.tsx b/app/components/bgRemoval/bg-removal-panel.tsx index 8c652b0..db3939e 100644 --- a/app/components/bgRemoval/bg-removal-panel.tsx +++ b/app/components/bgRemoval/bg-removal-panel.tsx @@ -278,6 +278,25 @@ export function BgPanel(props: FileProps) { const { pollTask } = useTaskPoller(); const { updateLocalUsage, getLocalData } = useLocalStorage(); + const handleGenerateImg = async () => { + if (!prompt.trim()) { + return message.error("请先输入提示词"); + } + setIsLoading(true); + 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(); + console.log(responseData.data.data[0]); + setPreviewUrl(responseData.data.data[0].url); + setIsLoading(false); + }; + const handleApiRequest = async (endpoint: string) => { if (!previewUrl) { message.error(Locale.BgRemoval.error.selectImg); @@ -391,6 +410,13 @@ export function BgPanel(props: FileProps) { <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 diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 2a33ac4..dfc6e24 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -53,6 +53,7 @@ import HeadphoneIcon from "../icons/headphone.svg"; import ExcelIcon from "../icons/excel.svg"; import WordIcon from "../icons/word.svg"; import MindIcon from "../icons/mind.svg"; +import PptIcon from "../icons/ppt.svg"; import { BOT_HELLO, @@ -1739,6 +1740,11 @@ function _Chat() { navigate("/mind", { state: { msg: true } }); } + //20250328新增PPT导出 + function toPowerpoint(pptMessage: string) { + navigate("/powerpoint", { state: { pptMessage: pptMessage, msg: true } }); + } + return ( <> <div className={styles.chat} key={session.id}> @@ -2019,6 +2025,15 @@ function _Chat() { /> </> )} + <ChatAction + text={Locale.Chat.Actions.Ppt} + icon={<PptIcon />} + onClick={() => + toPowerpoint( + getMessageTextContent(message), + ) + } + /> {/* 以上 20250317 新增Word excel导出按钮 */} {config.ttsConfig.enable && ( <ChatAction diff --git a/app/components/home.tsx b/app/components/home.tsx index 7cf09e7..b0cf84d 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -86,21 +86,29 @@ const McpMarketPage = dynamic( loading: () => <Loading noLogo />, }, ); -//以下新增思维导图路由,抠图路由 20250319 +//以下新增思维导图路由20250319 const Mind = dynamic(async () => (await import("./mind")).MindPage, { loading: () => <Loading noLogo />, }); +//以下新增抠图页面20250319 const BgRemoval = dynamic(async () => (await import("./bgRemoval")).BgRemoval, { loading: () => <Loading noLogo />, }); - +//以下新增写作页面20250325 const WritingPage = dynamic( async () => (await import("./writing")).WritingPage, { loading: () => <Loading noLogo />, }, ); +//以下新增PPT制作 页面 20250328 +const PowerPoint = dynamic( + async () => (await import("./powerpoint")).PowerPoint, + { + loading: () => <Loading noLogo />, + }, +); export function useSwitchTheme() { const config = useAppConfig(); @@ -189,6 +197,7 @@ function Screen() { const isMind = location.pathname === Path.Mind; const isBgRemoval = location.pathname === Path.BgRemoval; const isWrting = location.pathname === Path.Writing; + const isPowerpoint = location.pathname === Path.Powerpoint; const isMobileScreen = useMobileScreen(); const shouldTightBorder = @@ -215,6 +224,8 @@ function Screen() { if (isBgRemoval) return <BgRemoval />; //20250325新增AI写作界面 if (isWrting) return <WritingPage />; + //20250328新增ppt制作页面 + if (isPowerpoint) return <PowerPoint />; return ( <> diff --git a/app/components/mind/mind.tsx b/app/components/mind/mind.tsx index 2917530..4f24f8b 100644 --- a/app/components/mind/mind.tsx +++ b/app/components/mind/mind.tsx @@ -49,6 +49,8 @@ export function MindPage() { // 初始化配置项 const options: Options = { el: containerRef.current, + locale: "zh_CN", + draggable: true, contextMenu: true, toolBar: true, nodeMenu: true, @@ -66,7 +68,6 @@ export function MindPage() { newMessages, "gpt-4o-mini", ); - console.log("原始响应:", response); let cleanedContent = response.startsWith("```json") ? response.substring(8) : response; @@ -77,7 +78,6 @@ export function MindPage() { ); } const parsedData: MindElixirData = JSON.parse(cleanedContent); - console.log("解析后响应:", parsedData); // 增强校验逻辑 if ( !parsedData?.nodeData?.id || diff --git a/app/components/powerpoint/index.tsx b/app/components/powerpoint/index.tsx new file mode 100644 index 0000000..f442cc9 --- /dev/null +++ b/app/components/powerpoint/index.tsx @@ -0,0 +1 @@ +export * from "./powerpoint"; diff --git a/app/components/powerpoint/powerpoint.module.scss b/app/components/powerpoint/powerpoint.module.scss new file mode 100644 index 0000000..69f6619 --- /dev/null +++ b/app/components/powerpoint/powerpoint.module.scss @@ -0,0 +1,4 @@ +.container{ + width: 100%; + height: 100%; +} \ No newline at end of file diff --git a/app/components/powerpoint/powerpoint.tsx b/app/components/powerpoint/powerpoint.tsx new file mode 100644 index 0000000..cf470ef --- /dev/null +++ b/app/components/powerpoint/powerpoint.tsx @@ -0,0 +1,168 @@ +import styles from "./powerpoint.module.scss"; +import chatStyles from "@/app/components/chat.module.scss"; +import clsx from "clsx"; + +import { IconButton } from "../button"; +import Locale from "@/app/locales"; +import { useNavigate, useLocation } from "react-router-dom"; +import { useMobileScreen } from "@/app/utils"; +import { Path } from "@/app/constant"; +import { useRef, useEffect, useMemo } from "react"; +import { CreatorType, DocmeeUI } from "@docmee/sdk-ui"; +import { getClientConfig } from "@/app/config/client"; +import { useAppConfig, useAccessStore } from "@/app/store"; +import type { generateError, generateOutline } from "@/app/types/docmee"; +import axios from "axios"; +import ReturnIcon from "@/app/icons/return.svg"; +import MinIcon from "@/app/icons/min.svg"; +import MaxIcon from "@/app/icons/max.svg"; +import { message } from "antd"; + +// 错误消息映射 +const errorMap: { [key: number]: string } = { + [-1]: "操作失败", + [88]: "功能受限(积分已用完 或 非VIP)", + [98]: "认证失败(检查token是否过期)", + [99]: "登录过期", + [1001]: "数据不存在", + [1002]: "数据访问异常", + [1003]: "无权限访问", + [1006]: "内容涉及敏感信息", + [1009]: "AI服务异常", + [1010]: "你的次数已用完", + [1012]: "请求太频繁,限流", +}; + +// 获取错误消息的函数 +const getErrorMessage = (errorCode: number): string => { + return errorMap[errorCode] || `未知错误(错误码:${errorCode})`; +}; + +export function PowerPoint() { + const accessStore = useAccessStore(); + const containerRef = useRef<HTMLDivElement>(null); + const isPowerpoint = location.pathname === Path.Powerpoint; + const isMobileScreen = useMobileScreen(); + const navigate = useNavigate(); + const clientConfig = useMemo(() => getClientConfig(), []); + const showMaxIcon = !isMobileScreen && !clientConfig?.isApp; + const config = useAppConfig(); + const scrollRef = useRef<HTMLDivElement>(null); + + const query = useLocation(); + const { msg, pptMessage } = query.state || {}; + + const getToken = async () => { + if (!accessStore.accessCode) { + return message.error("请先输入登录秘钥"); + } + const res = await fetch("/api/ppt/createApiToken", { + method: "POST", + body: JSON.stringify(accessStore.accessCode), + }); + const data = await res.json(); + console.log(data); + if (data.status == 200) { + return data.data.data.token; + } else { + message.error(data.error || "获取 token 失败"); + } + return ""; + }; + + useEffect(() => { + const initializeDocmee = async () => { + let token = localStorage.getItem("token"); + // 如果本地没有token,则获取新token + if (!token) { + token = await getToken(); + if (!token) { + message.error("无效token请检查登录密码!"); + return navigate(Path.Settings); // 跳转回聊天页 + } + localStorage.setItem("token", token); + } + if (!containerRef.current) { + throw new Error("Container element not found"); + } + const docmee = new DocmeeUI({ + container: containerRef.current, + page: "creator-v2", + token: token, + mode: "light", + lang: "zh", + isMobile: isMobileScreen, + creatorData: { + type: CreatorType.AI_GEN, + subject: "Ai行业未来10年的发展预测", + }, + }); + if (msg) { + docmee.on("mounted", (msg: generateOutline) => { + docmee.changeCreatorData({ text: pptMessage }, true); + }); + } + docmee.on("beforeGenerate", (msg: generateOutline) => { + axios.post("https://docmee.cn/api/ppt/v2/generateContent", msg, { + headers: { token: token }, + }); + }); + docmee.on("error", (msg: generateError) => { + if (msg.data.code == 98) { + docmee.updateToken(token); + } + message.error(msg.data.message); + }); + return () => docmee.destroy(); + }; + initializeDocmee().catch(console.error); + }, [navigate, isMobileScreen, msg, pptMessage]); + + return ( + <> + <div style={{ width: "100%", height: "100%" }}> + <div className={chatStyles.chat} key={"1"}> + <div className="window-header" data-tauri-drag-region> + <div + className={clsx( + "window-header-title", + chatStyles["chat-body-title"], + )} + > + <div className={`window-header-main-title`}>PPT制作</div> + </div> + <div className={chatStyles["chat-message-actions"]}> + <div className={chatStyles["chat-input-actions"]}></div> + </div> + <div className="window-actions"> + <IconButton + aria="返回首页" + icon={<ReturnIcon />} + bordered + title={Locale.Chat.Actions.ChatList} + onClick={() => navigate(Path.Chat)} + /> + {showMaxIcon && ( + <div className="window-action-button"> + <IconButton + aria={Locale.Chat.Actions.FullScreen} + icon={config.tightBorder ? <MinIcon /> : <MaxIcon />} + bordered + onClick={() => { + config.update( + (config) => (config.tightBorder = !config.tightBorder), + ); + }} + /> + </div> + )} + </div> + </div> + <div className={chatStyles["chat-body"]} ref={scrollRef}> + <div ref={containerRef} className={styles["container"]}></div> + </div> + </div> + </div> + </> + ); +} diff --git a/app/components/powerpoint/ppt-siderbar.tsx b/app/components/powerpoint/ppt-siderbar.tsx new file mode 100644 index 0000000..4853234 --- /dev/null +++ b/app/components/powerpoint/ppt-siderbar.tsx @@ -0,0 +1,82 @@ +import { useMobileScreen } from "@/app/utils"; +import dynamic from "next/dynamic"; +import { + SideBarContainer, + SideBarHeader, + useDragSideBar, + useHotKey, +} from "@/app/components/sidebar"; +import { IconButton } from "@/app/components/button"; +import ReturnIcon from "@/app/icons/return.svg"; +import HistoryIcon from "@/app/icons/history.svg"; +import Locale from "@/app/locales"; +import { Path } from "@/app/constant"; +import { useNavigate } from "react-router-dom"; +import SDIcon from "@/app/icons/sd.svg"; + +const MindPanel = dynamic( + async () => (await import("@/app/components/mind")).MindPanel, + { + loading: () => null, + }, +); + +export function PptSiderBar(props: { className?: string }) { + const isMobileScreen = useMobileScreen(); + const { onDragStart, shouldNarrow } = useDragSideBar(); + const navigate = useNavigate(); + useHotKey(); + return ( + <> + <SideBarContainer + onDragStart={onDragStart} + shouldNarrow={shouldNarrow} + {...props} + > + {isMobileScreen ? ( + <div + className="window-header" + data-tauri-drag-region + style={{ + paddingLeft: 0, + paddingRight: 0, + }} + > + <div className="window-actions"> + <div className="window-action-button"> + <IconButton + icon={<ReturnIcon />} + bordered + title={Locale.Sd.Actions.ReturnHome} + onClick={() => navigate(Path.Home)} + /> + </div> + </div> + <SDIcon width={50} height={50} /> + <div className="window-actions"> + <div className="window-action-button"> + <IconButton + icon={<HistoryIcon />} + bordered + title={Locale.Sd.Actions.History} + onClick={() => navigate(Path.SdNew)} + /> + </div> + </div> + </div> + ) : ( + <SideBarHeader + title={ + <IconButton + icon={<ReturnIcon />} + bordered + title={Locale.Sd.Actions.ReturnHome} + onClick={() => navigate(Path.Home)} + /> + } + ></SideBarHeader> + )} + </SideBarContainer> + </> + ); +} diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 68ebcf0..80414d3 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -685,6 +685,10 @@ export function Settings() { type="text" placeholder={Locale.Settings.Access.AccessCode.Placeholder} onChange={(e) => { + console.log("更改密码了"); + if (localStorage.getItem("token")) { + localStorage.removeItem("token"); + } //20250328新增更改访问密码删除本地储存token accessStore.update( (access) => (access.accessCode = e.currentTarget.value), ); diff --git a/app/components/sidebar.tsx b/app/components/sidebar.tsx index dc64df8..c5fa35f 100644 --- a/app/components/sidebar.tsx +++ b/app/components/sidebar.tsx @@ -38,8 +38,9 @@ const DISCOVERY = [ { name: "Stable Diffusion", path: Path.Sd }, { name: Locale.SearchChat.Page.Title, path: Path.SearchChat }, { name: "智能抠图", path: Path.BgRemoval }, - { name: "AI-writing", path: Path.Writing }, + { name: "AI-Writing", path: Path.Writing }, { name: "思维导图", path: Path.Mind }, + { name: "AI-PPT", path: Path.Powerpoint }, ]; const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, { diff --git a/app/config/server.ts b/app/config/server.ts index 96c7de4..f0fb3db 100644 --- a/app/config/server.ts +++ b/app/config/server.ts @@ -98,11 +98,14 @@ declare global { BACKGROUND_REMOVAL_API_KEY: string; MAX_DAILY_USES: number; - NXET_PUBLIC_BGREMOVAL_MODEL : string; - NXET_PUBLIC_WRITING_MODEL :string; + DOCMEE_URL: string; + DOCMEE_API_KEY: string; + + NXET_PUBLIC_BGREMOVAL_MODEL: string; + NXET_PUBLIC_WRITING_MODEL: string; + } } } -} const ACCESS_CODES = (function getAccessCodes(): Set<string> { const code = process.env.CODE; @@ -124,7 +127,8 @@ function getApiKey(keys?: string) { const apiKey = apiKeys[randomIndex]; if (apiKey) { console.log( - `[Server Config] using ${randomIndex + 1} of ${apiKeys.length + `[Server Config] using ${randomIndex + 1} of ${ + apiKeys.length } api key - ${apiKey}`, ); } @@ -277,8 +281,12 @@ export const getServerSideConfig = () => { bgRemovalUrl: process.env.BACKGROUND_REMOVAL_URL ?? "", bgRemovalApiKey: process.env.BACKGROUND_REMOVAL_API_KEY ?? "", maxDailyUses: process.env.MAX_DAILY_USES, + //20250328新增 ppt api + docmeeUrl: process.env.DOCMEE_URL, + docmeeApiKey: process.env.DOCMEE_API_KEY ?? "", + docmeeMaxDailyUses: process.env.DOCMEE_MAX_DAILY_USES, - bgRemovalModel : process.env.NXET_PUBLIC_BGREMOVAL_MODEL, + bgRemovalModel: process.env.NXET_PUBLIC_BGREMOVAL_MODEL, writingModel: process.env.NXET_PUBLIC_WRITING_MODEL, }; }; diff --git a/app/constant.ts b/app/constant.ts index 3ce6b1f..f1dfe4c 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -54,8 +54,9 @@ export enum Path { McpMarket = "/mcp-market", //20250317新增路由 思维导图 Mind = "/mind", - BgRemoval="/background-removal", - Writing="/aiWriting" + BgRemoval = "/background-removal", + Writing = "/aiWriting", + Powerpoint = "/powerpoint", } export enum ApiPath { @@ -77,7 +78,9 @@ export enum ApiPath { DeepSeek = "/api/deepseek", SiliconFlow = "/api/siliconflow", //20250321 新增佐糖API - ZuoTang="/api/tasks" + ZuoTang = "/api/tasks", + Docmee = "/api/ppt", + OpenAiImg = "/api/v1", } ///api/tasks/visual/segmentation //api/tasks/visual/segmentation/{task_id} diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 2389ce0..6a88e68 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -1,4 +1,3 @@ -import { title } from "process"; import { getClientConfig } from "../config/client"; import { SubmitKey } from "../store/config"; import { SAAS_CHAT_UTM_URL } from "@/app/constant"; @@ -62,11 +61,11 @@ const cn = { //20250317新增 Word: "导出Word", Excel: "下载Excel", - Pdf:"导出PDF", - Ppt:"导出PPT", + Pdf: "导出PDF", + Ppt: "导出PPT", Mind: "生成思维导图", Drag: "拖动模式", - ReWrite:"重写", + ReWrite: "重写", }, Commands: { new: "新建聊天", @@ -860,26 +859,27 @@ const cn = { }, // 20250320新增 - BgRemoval:{ - Title:"智能抠图", - subTitle:"AI抠图", - error:{ - reqErr:"请求失败", - selectImg:"请选择图片", - code:"请先输入访问密码", - prompt:"请输入背景提示词", - resultErr:"结果图片加载失败", - downLoadErr:"请先完成图片处理", - statuErr:"状态查询失败", - timeoutErr:"处理超时,请稍后重试", - imgLoadingErr:"图片加载失败", - }, - success:"图片处理成功,请在一小时内保存图片!", - bgRemoveBtn:"一键抠图", - downloadImg:"下载图片", - generateBg:"生成背景", - promptTitle:"背景提示词", - } + BgRemoval: { + Title: "智能抠图", + subTitle: "AI抠图", + error: { + reqErr: "请求失败", + selectImg: "请选择图片", + code: "请先输入访问密码", + prompt: "请输入背景提示词", + resultErr: "结果图片加载失败", + downLoadErr: "请先完成图片处理", + statuErr: "状态查询失败", + timeoutErr: "处理超时,请稍后重试", + imgLoadingErr: "图片加载失败", + }, + success: "图片处理成功,请在一小时内保存图片!", + generateImg: "生成图片", + bgRemoveBtn: "一键抠图", + downloadImg: "下载图片", + generateBg: "生成背景", + promptTitle: "背景提示词", + }, }; type DeepPartial<T> = T extends object diff --git a/app/types/docmee.d.ts b/app/types/docmee.d.ts new file mode 100644 index 0000000..13c54b2 --- /dev/null +++ b/app/types/docmee.d.ts @@ -0,0 +1,24 @@ +export type generateOutline = { + data: { + fields: { + enableWeb: boolean; + length: string; + prompt: string; + subject: string; + _lang: string; + text?: string; + file?: File; + website?: string; + outline?: File; + }; + subtype: string; + }; + type: string; +}; +export type generateError = { + data: { + code: number; + message: string; + }; + type: string; +}; diff --git a/package.json b/package.json index c737041..5afbb43 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "test:ci": "node --no-warnings --experimental-vm-modules $(yarn bin jest) --ci" }, "dependencies": { + "@docmee/sdk-ui": "^1.1.17", "@fortaine/fetch-event-source": "^3.0.6", "@hello-pangea/dnd": "^16.5.0", "@modelcontextprotocol/sdk": "^1.0.4", diff --git a/yarn.lock b/yarn.lock index 861ba16..f91ca83 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1371,6 +1371,13 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" +"@docmee/sdk-ui@^1.1.17": + version "1.1.17" + resolved "https://registry.npmmirror.com/@docmee/sdk-ui/-/sdk-ui-1.1.17.tgz#549ee8b20dfe07eada422e9943b651d5a5196dd0" + integrity sha512-K/pWu2tg9ZrE9wbI5Naylh+LVd86kwMG7A9pu5urN1XjV+MAHj7ruOMrmqMkGbN5fVfFN8DbxWx4yUMcCMHTxA== + dependencies: + query-string "^9.1.1" + "@emotion/hash@^0.8.0": version "0.8.0" resolved "https://registry.npmmirror.com/@emotion/hash/-/hash-0.8.0.tgz" @@ -4266,6 +4273,11 @@ decode-named-character-reference@^1.0.0: dependencies: character-entities "^2.0.0" +decode-uri-component@^0.4.1: + version "0.4.1" + resolved "https://registry.npmmirror.com/decode-uri-component/-/decode-uri-component-0.4.1.tgz#2ac4859663c704be22bf7db760a1494a49ab2cc5" + integrity sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ== + dedent@^1.0.0: version "1.5.3" resolved "https://registry.npmmirror.com/dedent/-/dedent-1.5.3.tgz" @@ -5136,6 +5148,11 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +filter-obj@^5.1.0: + version "5.1.0" + resolved "https://registry.npmmirror.com/filter-obj/-/filter-obj-5.1.0.tgz#5bd89676000a713d7db2e197f660274428e524ed" + integrity sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng== + find-cache-dir@^3.3.1: version "3.3.2" resolved "https://registry.npmmirror.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz" @@ -8035,6 +8052,15 @@ pure-rand@^6.0.0: resolved "https://registry.npmmirror.com/pure-rand/-/pure-rand-6.1.0.tgz" integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== +query-string@^9.1.1: + version "9.1.1" + resolved "https://registry.npmmirror.com/query-string/-/query-string-9.1.1.tgz#dbfebb4196aeb2919915f2b2b81b91b965cf03a0" + integrity sha512-MWkCOVIcJP9QSKU52Ngow6bsAWAPlPK2MludXvcrS2bGZSl+T1qX9MZvRIkqUIkGLJquMJHWfsT6eRqUpp4aWg== + dependencies: + decode-uri-component "^0.4.1" + filter-obj "^5.1.0" + split-on-first "^3.0.0" + querystringify@^2.1.1: version "2.2.0" resolved "https://registry.npmmirror.com/querystringify/-/querystringify-2.2.0.tgz" @@ -9068,6 +9094,11 @@ spawn-command@0.0.2: resolved "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz" integrity sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ== +split-on-first@^3.0.0: + version "3.0.0" + resolved "https://registry.npmmirror.com/split-on-first/-/split-on-first-3.0.0.tgz#f04959c9ea8101b9b0bbf35a61b9ebea784a23e7" + integrity sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA== + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.0.3.tgz" -- libgit2 0.24.0