Spaces:
Running
Running
gpt-image-playground deploy commited on
Commit ·
e5a9556
1
Parent(s): da92fb7
Deploy 7db5c07 to Docker Space
Browse files
README.md
CHANGED
|
@@ -12,7 +12,7 @@ app_port: 4783
|
|
| 12 |
本地 AI 图片创作工作台,面向中文内容运营、设计草图和自动化生图流程。支持 `gpt-image-2`、OpenAI 兼容图片接口、文生图、图生图、遮罩编辑、批量任务、历史复用、费用追踪和 Agent API。
|
| 13 |
|
| 14 |
<p align="center">
|
| 15 |
-
<img src="https://raw.githubusercontent.com/MisonL/gpt-image-playground-customer/
|
| 16 |
</p>
|
| 17 |
|
| 18 |
## 快速开始
|
|
@@ -83,13 +83,13 @@ start-windows.bat
|
|
| 83 |
遮罩编辑示例:
|
| 84 |
|
| 85 |
<p align="center">
|
| 86 |
-
<img src="https://raw.githubusercontent.com/MisonL/gpt-image-playground-customer/
|
| 87 |
</p>
|
| 88 |
|
| 89 |
历史与费用示例:
|
| 90 |
|
| 91 |
<p align="center">
|
| 92 |
-
<img src="https://raw.githubusercontent.com/MisonL/gpt-image-playground-customer/
|
| 93 |
</p>
|
| 94 |
|
| 95 |
## 配置
|
|
|
|
| 12 |
本地 AI 图片创作工作台,面向中文内容运营、设计草图和自动化生图流程。支持 `gpt-image-2`、OpenAI 兼容图片接口、文生图、图生图、遮罩编辑、批量任务、历史复用、费用追踪和 Agent API。
|
| 13 |
|
| 14 |
<p align="center">
|
| 15 |
+
<img src="https://raw.githubusercontent.com/MisonL/gpt-image-playground-customer/7db5c07cd25ba69e829f2561814fec0a1a73e7e8/readme-images/interface.jpg?v=20260608-07b596b" alt="GPT Image Playground 界面" width="900"/>
|
| 16 |
</p>
|
| 17 |
|
| 18 |
## 快速开始
|
|
|
|
| 83 |
遮罩编辑示例:
|
| 84 |
|
| 85 |
<p align="center">
|
| 86 |
+
<img src="https://raw.githubusercontent.com/MisonL/gpt-image-playground-customer/7db5c07cd25ba69e829f2561814fec0a1a73e7e8/readme-images/mask-creation.jpg?v=20260608-07b596b" alt="遮罩创建" width="900"/>
|
| 87 |
</p>
|
| 88 |
|
| 89 |
历史与费用示例:
|
| 90 |
|
| 91 |
<p align="center">
|
| 92 |
+
<img src="https://raw.githubusercontent.com/MisonL/gpt-image-playground-customer/7db5c07cd25ba69e829f2561814fec0a1a73e7e8/readme-images/history.jpg?v=20260608-07b596b" alt="历史面板" width="900"/>
|
| 93 |
</p>
|
| 94 |
|
| 95 |
## 配置
|
public/hf-space-deploy-marker.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
{
|
| 2 |
"schema_version": 1,
|
| 3 |
-
"local_sha": "
|
| 4 |
-
"created_at": "2026-07-
|
| 5 |
-
"deploy_id": "
|
| 6 |
}
|
|
|
|
| 1 |
{
|
| 2 |
"schema_version": 1,
|
| 3 |
+
"local_sha": "7db5c07cd25ba69e829f2561814fec0a1a73e7e8",
|
| 4 |
+
"created_at": "2026-07-23T14:28:33.665Z",
|
| 5 |
+
"deploy_id": "e28ac5a2-35c9-4be8-a553-2f3632c02d2e"
|
| 6 |
}
|
src/app/api/deploy-marker/route.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
import { NextResponse } from 'next/server';
|
| 2 |
|
| 3 |
-
const deployMarker = {"schema_version":1,"local_sha":"
|
| 4 |
|
| 5 |
export const dynamic = 'force-dynamic';
|
| 6 |
|
|
|
|
| 1 |
import { NextResponse } from 'next/server';
|
| 2 |
|
| 3 |
+
const deployMarker = {"schema_version":1,"local_sha":"7db5c07cd25ba69e829f2561814fec0a1a73e7e8","created_at":"2026-07-23T14:28:33.665Z","deploy_id":"e28ac5a2-35c9-4be8-a553-2f3632c02d2e"} as const;
|
| 4 |
|
| 5 |
export const dynamic = 'force-dynamic';
|
| 6 |
|
src/components/password-dialog.test.tsx
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { I18nProvider } from '@/lib/i18n';
|
| 2 |
+
import { renderInClientDom } from '@/test-utils/react-dom';
|
| 3 |
+
import assert from 'node:assert/strict';
|
| 4 |
+
import { after, before, describe, it } from 'node:test';
|
| 5 |
+
|
| 6 |
+
type PasswordDialogComponent = typeof import('./password-dialog').PasswordDialog;
|
| 7 |
+
|
| 8 |
+
let PasswordDialog: PasswordDialogComponent;
|
| 9 |
+
let sharedView: Awaited<ReturnType<typeof renderInClientDom>>;
|
| 10 |
+
let ownerDocument: Document;
|
| 11 |
+
|
| 12 |
+
before(async () => {
|
| 13 |
+
sharedView = await renderInClientDom(<div />);
|
| 14 |
+
ownerDocument = sharedView.container.ownerDocument;
|
| 15 |
+
({ PasswordDialog } = await import('./password-dialog'));
|
| 16 |
+
});
|
| 17 |
+
|
| 18 |
+
after(async () => {
|
| 19 |
+
await sharedView?.cleanup();
|
| 20 |
+
});
|
| 21 |
+
|
| 22 |
+
describe('PasswordDialog', { concurrency: false }, () => {
|
| 23 |
+
it('associates the access-code input and save action with a native form', async () => {
|
| 24 |
+
await sharedView.render(
|
| 25 |
+
<I18nProvider>
|
| 26 |
+
<PasswordDialog
|
| 27 |
+
isOpen
|
| 28 |
+
onOpenChange={() => {}}
|
| 29 |
+
onSave={() => {}}
|
| 30 |
+
description='Configure the access code for this test.'
|
| 31 |
+
/>
|
| 32 |
+
</I18nProvider>
|
| 33 |
+
);
|
| 34 |
+
|
| 35 |
+
const input = ownerDocument.querySelector<HTMLInputElement>('#password-input');
|
| 36 |
+
const form = ownerDocument.querySelector<HTMLFormElement>('form');
|
| 37 |
+
const saveButton = [...ownerDocument.querySelectorAll<HTMLButtonElement>('button')].find(
|
| 38 |
+
(button) => button.textContent === '保存'
|
| 39 |
+
);
|
| 40 |
+
|
| 41 |
+
assert.ok(input, 'missing access code input');
|
| 42 |
+
assert.ok(form, 'missing access code form');
|
| 43 |
+
assert.ok(saveButton, 'missing save button');
|
| 44 |
+
assert.equal(input.form, form);
|
| 45 |
+
assert.equal(saveButton.type, 'submit');
|
| 46 |
+
});
|
| 47 |
+
});
|
src/components/password-dialog.tsx
CHANGED
|
@@ -40,6 +40,12 @@ export function PasswordDialog({
|
|
| 40 |
onOpenChange(false);
|
| 41 |
};
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
const handleDialogClose = (open: boolean) => {
|
| 44 |
if (!open) {
|
| 45 |
setCurrentPassword('');
|
|
@@ -54,35 +60,31 @@ export function PasswordDialog({
|
|
| 54 |
<DialogTitle>{title}</DialogTitle>
|
| 55 |
{description && <DialogDescription>{description}</DialogDescription>}
|
| 56 |
</DialogHeader>
|
| 57 |
-
<
|
| 58 |
-
<div className='grid
|
| 59 |
-
<
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
handleSave();
|
| 76 |
-
}
|
| 77 |
-
}}
|
| 78 |
-
/>
|
| 79 |
</div>
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
</
|
| 85 |
-
</
|
| 86 |
</DialogContent>
|
| 87 |
</Dialog>
|
| 88 |
);
|
|
|
|
| 40 |
onOpenChange(false);
|
| 41 |
};
|
| 42 |
|
| 43 |
+
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
| 44 |
+
event.preventDefault();
|
| 45 |
+
if (!currentPassword.trim()) return;
|
| 46 |
+
handleSave();
|
| 47 |
+
};
|
| 48 |
+
|
| 49 |
const handleDialogClose = (open: boolean) => {
|
| 50 |
if (!open) {
|
| 51 |
setCurrentPassword('');
|
|
|
|
| 60 |
<DialogTitle>{title}</DialogTitle>
|
| 61 |
{description && <DialogDescription>{description}</DialogDescription>}
|
| 62 |
</DialogHeader>
|
| 63 |
+
<form onSubmit={handleSubmit} className='grid gap-4'>
|
| 64 |
+
<div className='grid gap-4 py-4'>
|
| 65 |
+
<div className='grid grid-cols-1 items-center gap-4'>
|
| 66 |
+
<Label htmlFor='password-input' className='sr-only'>
|
| 67 |
+
{t('password.placeholder')}
|
| 68 |
+
</Label>
|
| 69 |
+
<Input
|
| 70 |
+
ref={inputRef}
|
| 71 |
+
id='password-input'
|
| 72 |
+
name='password'
|
| 73 |
+
type='password'
|
| 74 |
+
autoComplete='current-password'
|
| 75 |
+
placeholder={t('password.placeholder')}
|
| 76 |
+
value={currentPassword}
|
| 77 |
+
onChange={(e) => setCurrentPassword(e.target.value)}
|
| 78 |
+
className='col-span-1'
|
| 79 |
+
/>
|
| 80 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
</div>
|
| 82 |
+
<DialogFooter>
|
| 83 |
+
<Button type='submit' disabled={!currentPassword.trim()} className='px-6'>
|
| 84 |
+
{t('common.save')}
|
| 85 |
+
</Button>
|
| 86 |
+
</DialogFooter>
|
| 87 |
+
</form>
|
| 88 |
</DialogContent>
|
| 89 |
</Dialog>
|
| 90 |
);
|