mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-08-05 17:04:27 +08:00
feat: update project structure for Vite and React Router, remove deprecated sources, and enhance Docker deployment
This commit is contained in:
+8
-2
@@ -2,9 +2,15 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
+ [新增] 渠道兼容Gemini格式。
|
|
||||||
+ [新增] 新增Codex App插件支持。
|
+ [新增] 新增Codex App插件支持。
|
||||||
+ [调整] Next.js -> Vite,前端构建工具迁移。
|
+ [修复] 修复前端 TypeScript 构建报错。
|
||||||
|
+ [调整] Docker 运行镜像改为 nginx 静态托管。
|
||||||
|
|
||||||
|
## v0.5.0 - 2026-07-05
|
||||||
|
|
||||||
|
+ [新增] 渠道兼容Gemini格式。
|
||||||
|
+ [调整] 前端从 Next.js 迁移到 Vite,项目改为静态前端构建。
|
||||||
|
+ [调整] 移除已 404 的 EvoLinkAI 提示词来源。
|
||||||
|
|
||||||
## v0.4.0 - 2026-06-16
|
## v0.4.0 - 2026-06-16
|
||||||
|
|
||||||
|
|||||||
+3
-7
@@ -10,13 +10,9 @@ COPY web ./
|
|||||||
RUN bun run build
|
RUN bun run build
|
||||||
|
|
||||||
# 运行镜像:只启动静态前端,AI 请求由浏览器前台直连用户自己的接口。
|
# 运行镜像:只启动静态前端,AI 请求由浏览器前台直连用户自己的接口。
|
||||||
FROM node:22-bookworm-slim
|
FROM nginx:1.27-alpine
|
||||||
|
|
||||||
WORKDIR /app
|
COPY --from=web-build /app/web/dist /usr/share/nginx/html
|
||||||
COPY --from=web-build /app/web/dist /app/web/dist
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
ENV NODE_ENV=production
|
|
||||||
ENV PORT=3000
|
|
||||||
RUN npm install -g serve@14.2.5
|
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD ["serve", "-s", "/app/web/dist", "-l", "3000"]
|
|
||||||
|
|||||||
@@ -11,4 +11,8 @@ description: 当前版本已实现但仍需人工验证的变更项
|
|||||||
- 音频和视频生成:选择 Gemini 调用格式时先给出不支持提示,仍需使用 OpenAI 兼容渠道。
|
- 音频和视频生成:选择 Gemini 调用格式时先给出不支持提示,仍需使用 OpenAI 兼容渠道。
|
||||||
- WebDAV 同步:移除 Next.js 转发代理和连接方式选择,改为浏览器前端直接连接 WebDAV 服务。
|
- WebDAV 同步:移除 Next.js 转发代理和连接方式选择,改为浏览器前端直接连接 WebDAV 服务。
|
||||||
- 提示词库:移除 Next.js route 抓取和服务端内存缓存,改为浏览器前端直连 GitHub 原始文件并缓存到 IndexedDB。
|
- 提示词库:移除 Next.js route 抓取和服务端内存缓存,改为浏览器前端直连 GitHub 原始文件并缓存到 IndexedDB。
|
||||||
|
- 提示词库:移除已 404 的 `EvoLinkAI/awesome-gpt-image-2-API-and-Prompts` 来源。
|
||||||
- 前端构建:移除 Next.js,改为 Vite + React Router,并将前端目录调整为 `pages` / `components` / `stores` / `lib` / `layouts` / `styles`,需验证首页、图片/视频工作台、素材、提示词库和画布路由跳转。
|
- 前端构建:移除 Next.js,改为 Vite + React Router,并将前端目录调整为 `pages` / `components` / `stores` / `lib` / `layouts` / `styles`,需验证首页、图片/视频工作台、素材、提示词库和画布路由跳转。
|
||||||
|
- 前端构建:修复 TypeScript 严格检查下的构建报错。
|
||||||
|
- Vercel 部署:新增 `web` 和 `docs` 两个子项目的 Vercel 配置,需分别验证静态前端和文档站部署。
|
||||||
|
- Docker 部署:运行镜像从 Node serve 改为 nginx 静态托管,需验证前端路由刷新回退。
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"framework": "nextjs",
|
||||||
|
"installCommand": "bun install",
|
||||||
|
"buildCommand": "bun run build"
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
server {
|
||||||
|
listen 3000;
|
||||||
|
server_name _;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ export type InsertAssetPayload = { kind: "text"; content: string; title: string
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
defaultTab?: string;
|
||||||
onInsert: (payload: InsertAssetPayload) => void;
|
onInsert: (payload: InsertAssetPayload) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -805,7 +805,7 @@ function OnlineAgentLogView({ logs, theme, context, onClear }: { logs: OnlineAge
|
|||||||
const content = mode === "text" ? formatOnlineLogText(logs, context) : formatOnlineLogJson(logs, context);
|
const content = mode === "text" ? formatOnlineLogText(logs, context) : formatOnlineLogJson(logs, context);
|
||||||
const lastError = [...logs].reverse().find((item) => /错误|失败|error/i.test(`${item.title}\n${stringifyLog(item.data)}`));
|
const lastError = [...logs].reverse().find((item) => /错误|失败|error/i.test(`${item.title}\n${stringifyLog(item.data)}`));
|
||||||
const copy = async (value = content) => {
|
const copy = async (value = content) => {
|
||||||
if (copyToClipboard(value)) return;
|
if (await copyToClipboard(value)) return;
|
||||||
textareaRef.current?.focus();
|
textareaRef.current?.focus();
|
||||||
textareaRef.current?.select();
|
textareaRef.current?.select();
|
||||||
};
|
};
|
||||||
@@ -1266,7 +1266,7 @@ async function buildToolAgentMessages(snapshot: CanvasAgentSnapshot, history: Ca
|
|||||||
...history
|
...history
|
||||||
.filter((message) => message.role === "user" || message.role === "assistant" || message.role === "system")
|
.filter((message) => message.role === "user" || message.role === "assistant" || message.role === "system")
|
||||||
.slice(-8)
|
.slice(-8)
|
||||||
.map((message): ResponseInputMessage => ({ role: message.role, content: message.text })),
|
.map((message): ResponseInputMessage => ({ role: message.role as "system" | "user" | "assistant", content: message.text })),
|
||||||
{
|
{
|
||||||
role: "user",
|
role: "user",
|
||||||
content: [
|
content: [
|
||||||
|
|||||||
@@ -100,10 +100,12 @@ export function ImageToolSettingsModal({
|
|||||||
frames.push(firstFrame);
|
frames.push(firstFrame);
|
||||||
const timer = window.setTimeout(sync, 120);
|
const timer = window.setTimeout(sync, 120);
|
||||||
const resizeObserver = typeof ResizeObserver !== "undefined" && toolbar ? new ResizeObserver(sync) : null;
|
const resizeObserver = typeof ResizeObserver !== "undefined" && toolbar ? new ResizeObserver(sync) : null;
|
||||||
resizeObserver?.observe(toolbar);
|
if (resizeObserver && toolbar) {
|
||||||
toolbar?.childNodes.forEach((child) => {
|
resizeObserver.observe(toolbar);
|
||||||
if (child instanceof Element) resizeObserver?.observe(child);
|
toolbar.childNodes.forEach((child) => {
|
||||||
});
|
if (child instanceof Element) resizeObserver.observe(child);
|
||||||
|
});
|
||||||
|
}
|
||||||
sync();
|
sync();
|
||||||
window.addEventListener("resize", syncPreviewScroll);
|
window.addEventListener("resize", syncPreviewScroll);
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ export function CanvasNodeHoverToolbar({
|
|||||||
|
|
||||||
if (!node) return null;
|
if (!node) return null;
|
||||||
|
|
||||||
|
const activeNode = node;
|
||||||
const left = viewport.x + (node.position.x + node.width / 2) * viewport.k;
|
const left = viewport.x + (node.position.x + node.width / 2) * viewport.k;
|
||||||
const top = viewport.y + node.position.y * viewport.k - 14;
|
const top = viewport.y + node.position.y * viewport.k - 14;
|
||||||
const isImage = node.type === CanvasNodeType.Image;
|
const isImage = node.type === CanvasNodeType.Image;
|
||||||
@@ -124,7 +125,7 @@ export function CanvasNodeHoverToolbar({
|
|||||||
const imageTools = buildImageToolbarTools(node, { onUpload, onToggleFreeResize, onMaskEdit, onCrop, onSplit, onUpscale, onSuperResolve, onAngle, onViewImage, onCopyPrompt: copyImagePrompt, onReversePrompt });
|
const imageTools = buildImageToolbarTools(node, { onUpload, onToggleFreeResize, onMaskEdit, onCrop, onSplit, onUpscale, onSuperResolve, onAngle, onViewImage, onCopyPrompt: copyImagePrompt, onReversePrompt });
|
||||||
|
|
||||||
function openImageToolSettings() {
|
function openImageToolSettings() {
|
||||||
onKeep(node.id);
|
onKeep(activeNode.id);
|
||||||
setDraftImageToolIds(quickImageToolIds);
|
setDraftImageToolIds(quickImageToolIds);
|
||||||
setDraftShowImageToolLabels(showImageToolLabels);
|
setDraftShowImageToolLabels(showImageToolLabels);
|
||||||
setImageToolSettingsOpen(true);
|
setImageToolSettingsOpen(true);
|
||||||
@@ -280,7 +281,7 @@ export function CanvasNodeInfoModal({ node, open, onClose }: { node: CanvasNodeD
|
|||||||
function ToolbarAction({ title, label, icon, onClick, showLabel, active = false, danger = false }: ToolbarTool & { showLabel: boolean }) {
|
function ToolbarAction({ title, label, icon, onClick, showLabel, active = false, danger = false }: ToolbarTool & { showLabel: boolean }) {
|
||||||
const hasText = showLabel && Boolean(label);
|
const hasText = showLabel && Boolean(label);
|
||||||
return (
|
return (
|
||||||
<Tooltip title={title} placement="top" mouseEnterDelay={0.2} color="#ffffff" styles={{ body: { color: "#242529", boxShadow: "0 8px 24px rgba(15,23,42,.16)", fontSize: 13, fontWeight: 500 } }}>
|
<Tooltip title={title} placement="top" mouseEnterDelay={0.2} color="#ffffff" styles={{ root: { color: "#242529", boxShadow: "0 8px 24px rgba(15,23,42,.16)", fontSize: 13, fontWeight: 500 } }}>
|
||||||
<button type="button" className={`group relative flex h-12 items-center whitespace-nowrap px-1.5 ${danger ? "text-[#ef4444]" : ""}`} onClick={onClick} aria-label={title}>
|
<button type="button" className={`group relative flex h-12 items-center whitespace-nowrap px-1.5 ${danger ? "text-[#ef4444]" : ""}`} onClick={onClick} aria-label={title}>
|
||||||
<span className={`flex h-9 items-center ${hasText ? "gap-2 px-2.5" : "justify-center px-2"} rounded-lg transition group-hover:bg-[#f0f0f1] ${active ? "bg-[#eeeeef]" : ""}`}>
|
<span className={`flex h-9 items-center ${hasText ? "gap-2 px-2.5" : "justify-center px-2"} rounded-lg transition group-hover:bg-[#f0f0f1] ${active ? "bg-[#eeeeef]" : ""}`}>
|
||||||
{icon}
|
{icon}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export function CanvasNodeMaskEditDialog({ dataUrl, open, onClose, onConfirm }:
|
|||||||
const point = readCanvasPoint(event.currentTarget, event.clientX, event.clientY);
|
const point = readCanvasPoint(event.currentTarget, event.clientX, event.clientY);
|
||||||
const maskCanvas = maskCanvasRef.current;
|
const maskCanvas = maskCanvasRef.current;
|
||||||
const context = maskCanvas?.getContext("2d");
|
const context = maskCanvas?.getContext("2d");
|
||||||
if (!context) return;
|
if (!maskCanvas || !context) return;
|
||||||
context.lineCap = "round";
|
context.lineCap = "round";
|
||||||
context.lineJoin = "round";
|
context.lineJoin = "round";
|
||||||
context.lineWidth = brushSize;
|
context.lineWidth = brushSize;
|
||||||
|
|||||||
@@ -377,8 +377,9 @@ function consumeResponseStreamText(state: ResponseStreamState, text: string, onD
|
|||||||
for (;;) {
|
for (;;) {
|
||||||
const match = state.buffer.match(/\r?\n\r?\n/);
|
const match = state.buffer.match(/\r?\n\r?\n/);
|
||||||
if (!match) break;
|
if (!match) break;
|
||||||
consumeResponseStreamBlock(state.buffer.slice(0, match.index), state, onDelta);
|
const index = match.index ?? 0;
|
||||||
state.buffer = state.buffer.slice(match.index + match[0].length);
|
consumeResponseStreamBlock(state.buffer.slice(0, index), state, onDelta);
|
||||||
|
state.buffer = state.buffer.slice(index + match[0].length);
|
||||||
}
|
}
|
||||||
if (flush && state.buffer.trim()) {
|
if (flush && state.buffer.trim()) {
|
||||||
consumeResponseStreamBlock(state.buffer, state, onDelta);
|
consumeResponseStreamBlock(state.buffer, state, onDelta);
|
||||||
@@ -525,8 +526,9 @@ function consumeGeminiStreamText(state: GeminiStreamState, text: string, onDelta
|
|||||||
for (;;) {
|
for (;;) {
|
||||||
const match = state.buffer.match(/\r?\n\r?\n/);
|
const match = state.buffer.match(/\r?\n\r?\n/);
|
||||||
if (!match) break;
|
if (!match) break;
|
||||||
consumeGeminiStreamBlock(state.buffer.slice(0, match.index), state, onDelta);
|
const index = match.index ?? 0;
|
||||||
state.buffer = state.buffer.slice(match.index + match[0].length);
|
consumeGeminiStreamBlock(state.buffer.slice(0, index), state, onDelta);
|
||||||
|
state.buffer = state.buffer.slice(index + match[0].length);
|
||||||
}
|
}
|
||||||
if (flush && state.buffer.trim()) {
|
if (flush && state.buffer.trim()) {
|
||||||
consumeGeminiStreamBlock(state.buffer, state, onDelta);
|
consumeGeminiStreamBlock(state.buffer, state, onDelta);
|
||||||
|
|||||||
@@ -28,19 +28,16 @@ export type PromptListResponse = {
|
|||||||
total: number;
|
total: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const gptImage2RawBase = "https://raw.githubusercontent.com/EvoLinkAI/awesome-gpt-image-2-API-and-Prompts/main";
|
|
||||||
const awesomeGptImageRawBase = "https://raw.githubusercontent.com/ZeroLu/awesome-gpt-image/main";
|
const awesomeGptImageRawBase = "https://raw.githubusercontent.com/ZeroLu/awesome-gpt-image/main";
|
||||||
const awesomeGpt4oImagePromptsBase = "https://raw.githubusercontent.com/ImgEdify/Awesome-GPT4o-Image-Prompts/main";
|
const awesomeGpt4oImagePromptsBase = "https://raw.githubusercontent.com/ImgEdify/Awesome-GPT4o-Image-Prompts/main";
|
||||||
const youMindGptImage2RawBase = "https://raw.githubusercontent.com/YouMind-OpenLab/awesome-gpt-image-2/main";
|
const youMindGptImage2RawBase = "https://raw.githubusercontent.com/YouMind-OpenLab/awesome-gpt-image-2/main";
|
||||||
const youMindNanoBananaProRawBase = "https://raw.githubusercontent.com/YouMind-OpenLab/awesome-nano-banana-pro-prompts/main";
|
const youMindNanoBananaProRawBase = "https://raw.githubusercontent.com/YouMind-OpenLab/awesome-nano-banana-pro-prompts/main";
|
||||||
const davidWuGptImage2RawBase = "https://raw.githubusercontent.com/davidwuw0811-boop/awesome-gpt-image2-prompts/main";
|
const davidWuGptImage2RawBase = "https://raw.githubusercontent.com/davidwuw0811-boop/awesome-gpt-image2-prompts/main";
|
||||||
const gptImage2CaseFiles = ["README.md", "cases/ad-creative.md", "cases/character.md", "cases/comparison.md", "cases/ecommerce.md", "cases/portrait.md", "cases/poster.md", "cases/ui.md"];
|
|
||||||
const cacheTtlMs = 1000 * 60 * 60;
|
const cacheTtlMs = 1000 * 60 * 60;
|
||||||
const promptCacheKey = "third-party-prompts";
|
const promptCacheKey = "third-party-prompts";
|
||||||
const promptCacheStore = localforage.createInstance({ name: "infinite-canvas", storeName: "prompt_cache" });
|
const promptCacheStore = localforage.createInstance({ name: "infinite-canvas", storeName: "prompt_cache" });
|
||||||
|
|
||||||
const categories: PromptCategory[] = [
|
const categories: PromptCategory[] = [
|
||||||
{ category: "gpt-image-2-prompts", githubUrl: "https://github.com/EvoLinkAI/awesome-gpt-image-2-API-and-Prompts", build: buildGptImage2Prompts },
|
|
||||||
{ category: "awesome-gpt-image", githubUrl: "https://github.com/ZeroLu/awesome-gpt-image", build: buildAwesomeGptImagePrompts },
|
{ category: "awesome-gpt-image", githubUrl: "https://github.com/ZeroLu/awesome-gpt-image", build: buildAwesomeGptImagePrompts },
|
||||||
{ category: "awesome-gpt4o-image-prompts", githubUrl: "https://github.com/ImgEdify/Awesome-GPT4o-Image-Prompts", build: buildAwesomeGpt4oImagePrompts },
|
{ category: "awesome-gpt4o-image-prompts", githubUrl: "https://github.com/ImgEdify/Awesome-GPT4o-Image-Prompts", build: buildAwesomeGpt4oImagePrompts },
|
||||||
{ category: "youmind-gpt-image-2", githubUrl: "https://github.com/YouMind-OpenLab/awesome-gpt-image-2", build: () => buildYouMindPrompts(youMindGptImage2RawBase, "youmind-gpt-image-2", "gpt-image-2") },
|
{ category: "youmind-gpt-image-2", githubUrl: "https://github.com/YouMind-OpenLab/awesome-gpt-image-2", build: () => buildYouMindPrompts(youMindGptImage2RawBase, "youmind-gpt-image-2", "gpt-image-2") },
|
||||||
@@ -101,27 +98,6 @@ function filterPrompts(items: Prompt[], options: { keyword: string; category: st
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildGptImage2Prompts() {
|
|
||||||
const data = (await fetchJson<{ records?: Array<{ title?: string; tweet_url?: string; image_dir?: string; category?: string; added_at?: string }> }>(gptImage2RawBase, "data/ingested_tweets.json")).records || [];
|
|
||||||
const cases = new Map<string, string>();
|
|
||||||
const markdowns = await Promise.all(gptImage2CaseFiles.map((file) => fetchText(gptImage2RawBase, file)));
|
|
||||||
markdowns.forEach((markdown) => collectGptImage2Cases(cases, markdown));
|
|
||||||
const items: Omit<Prompt, "category" | "githubUrl">[] = [];
|
|
||||||
data.forEach((item) => {
|
|
||||||
const prompt = cases.get(item.tweet_url || "");
|
|
||||||
if (!item.title || !prompt || !item.image_dir) return;
|
|
||||||
const image = `${gptImage2RawBase}/${item.image_dir}/output.jpg`;
|
|
||||||
items.push({ id: `gpt-image-2-prompts-${leftPad(items.length + 1)}`, title: item.title, coverUrl: image, prompt, tags: tagsFromCategory(item.category || ""), preview: markdownPreview([image]), createdAt: item.added_at || "", updatedAt: item.added_at || "" });
|
|
||||||
});
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
function collectGptImage2Cases(cases: Map<string, string>, markdown: string) {
|
|
||||||
for (const match of markdown.matchAll(/### Case \d+: \[[^\]]+]\(([^)]+)\).*?\*\*Prompt:\*\*\s*\r?\n\s*```[\w-]*\r?\n(.*?)\r?\n```/gs)) {
|
|
||||||
cases.set(match[1], match[2].trim());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function buildAwesomeGptImagePrompts() {
|
async function buildAwesomeGptImagePrompts() {
|
||||||
const markdown = await fetchText(awesomeGptImageRawBase, "README.zh-CN.md");
|
const markdown = await fetchText(awesomeGptImageRawBase, "README.zh-CN.md");
|
||||||
const items: Omit<Prompt, "category" | "githubUrl">[] = [];
|
const items: Omit<Prompt, "category" | "githubUrl">[] = [];
|
||||||
|
|||||||
@@ -296,8 +296,8 @@ export function normalizeModelOptionValue(value: string | undefined, channels: M
|
|||||||
const channel = channels.find((item) => item.id === decoded.channelId);
|
const channel = channels.find((item) => item.id === decoded.channelId);
|
||||||
return channel && channel.models.includes(decoded.model) ? model : "";
|
return channel && channel.models.includes(decoded.model) ? model : "";
|
||||||
}
|
}
|
||||||
const channel = channels.find((item) => item.models.includes(decoded?.model || model)) || channels[0];
|
const channel = channels.find((item) => item.models.includes(model)) || channels[0];
|
||||||
return channel && channel.models.includes(decoded?.model || model) ? encodeChannelModel(channel.id, decoded?.model || model) : model;
|
return channel && channel.models.includes(model) ? encodeChannelModel(channel.id, model) : model;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveModelChannel(config: AiConfig, value: string) {
|
export function resolveModelChannel(config: AiConfig, value: string) {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2017",
|
"target": "ES2018",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"framework": "vite",
|
||||||
|
"installCommand": "bun install",
|
||||||
|
"buildCommand": "bun run build",
|
||||||
|
"outputDirectory": "dist",
|
||||||
|
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user