html2pptx-local-mcp 1.1.34 → 1.1.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/docs/content.js +42 -0
- package/lib/pptx-studio-mcp-core.js +54 -0
- package/local-editor-app/app/edit-slide/edit-slide-client.jsx +59 -34
- package/local-editor-app/components/studio/edit-property-panel.jsx +83 -85
- package/local-editor-app/lib/edit-slide-editor-helpers.js +12 -0
- package/mcp/pptx-studio-mcp-server.mjs +20 -0
- package/package.json +1 -1
package/app/docs/content.js
CHANGED
|
@@ -618,6 +618,26 @@ export const DOCS_COPY = {
|
|
|
618
618
|
],
|
|
619
619
|
|
|
620
620
|
apiEndpoints: [
|
|
621
|
+
{
|
|
622
|
+
method: 'POST',
|
|
623
|
+
path: '/api/v1/import/pptx',
|
|
624
|
+
title: 'Import PPTX to HTML',
|
|
625
|
+
description: 'Converts an existing .pptx into editable HTML slides (the reverse of export). Resolves theme colors (including clrMap/clrMapOvr inversion on dark slides), master text styles, and font families. Synchronous — returns the HTML in a single response. Also available at POST /api/import/pptx.',
|
|
626
|
+
requestTitle: 'Request Body',
|
|
627
|
+
requestFields: [
|
|
628
|
+
{ field: 'file', type: 'file', required: false, default: '--', description: 'multipart/form-data: the .pptx file (max 25 MB). Use this OR pptxBase64.' },
|
|
629
|
+
{ field: 'pptxBase64', type: 'string', required: false, default: '--', description: 'application/json: base64-encoded .pptx contents (data URLs accepted). Recommended for JSON-RPC / MCP clients.' },
|
|
630
|
+
{ field: 'fileName', type: 'string', required: false, default: 'presentation.pptx', description: 'Optional original file name used for labeling.' },
|
|
631
|
+
],
|
|
632
|
+
responseTitle: 'Response (200 OK)',
|
|
633
|
+
responseCode: '{\n "fileName": "deck.pptx",\n "plan": "api_starter",\n "slideCount": 13,\n "slides": [{ "index": 1, "html": "<section class=\\"slide\\">...</section>" }],\n "html": "<!doctype html>...",\n "css": "...",\n "warnings": []\n}',
|
|
634
|
+
errors: [
|
|
635
|
+
{ code: '400', description: 'Missing/invalid file, invalid base64, or invalid OOXML.' },
|
|
636
|
+
{ code: '401', description: 'Missing API key. Set the Authorization: Bearer or X-API-Key header.' },
|
|
637
|
+
{ code: '403', description: 'API key is not mapped to a plan with API access.' },
|
|
638
|
+
{ code: '413', description: 'PPTX exceeds the 25 MB size limit.' },
|
|
639
|
+
],
|
|
640
|
+
},
|
|
621
641
|
{
|
|
622
642
|
method: 'POST',
|
|
623
643
|
path: '/api/export/jobs',
|
|
@@ -987,6 +1007,7 @@ echo 'HTML2PPTX_API_KEY=sk_live_xxxx' >> .env`,
|
|
|
987
1007
|
mcpTools: [
|
|
988
1008
|
{ tool: 'html2pptx_list_export_plans', description: 'List the current commercial plan catalog and recommended plan.' },
|
|
989
1009
|
{ tool: 'html2pptx_create_export_job', description: 'Create an export job from HTML/CSS content. Supports optional width, height, layout, waitForCompletion, timeoutMs, and responseFormat ("url" | "base64" | "both").' },
|
|
1010
|
+
{ tool: 'html2pptx_import_pptx', description: 'Reverse of export: convert an existing .pptx into editable HTML slides. Pass the file as pptxBase64 (works on remote MCP); the local stdio MCP also accepts filePath. Resolves theme colors, master text styles, and fonts.' },
|
|
990
1011
|
{ tool: 'html2pptx_get_export_job', description: 'Get the current status of an export job by jobId.' },
|
|
991
1012
|
{ tool: 'html2pptx_wait_for_export_job', description: 'Poll until the job completes or fails. Handles retries and backoff inside the tool.' },
|
|
992
1013
|
{ tool: 'html2pptx_get_docs', description: 'Fetch html2pptx.app documentation to understand the API contract, HTML requirements, and integration guides.' },
|
|
@@ -1523,6 +1544,26 @@ echo 'HTML2PPTX_API_KEY=sk_live_xxxx' >> .env`,
|
|
|
1523
1544
|
],
|
|
1524
1545
|
|
|
1525
1546
|
apiEndpoints: [
|
|
1547
|
+
{
|
|
1548
|
+
method: 'POST',
|
|
1549
|
+
path: '/api/v1/import/pptx',
|
|
1550
|
+
title: 'PPTXをHTMLに取り込み',
|
|
1551
|
+
description: '既存の .pptx を編集可能なHTMLスライドへ変換します(エクスポートの逆方向)。テーマ色(ダークスライドの clrMap/clrMapOvr 反転を含む)、マスターのテキストスタイル、フォントファミリーを解決します。同期処理で、1回のレスポンスでHTMLを返します。POST /api/import/pptx でも利用可能。',
|
|
1552
|
+
requestTitle: 'リクエストボディ',
|
|
1553
|
+
requestFields: [
|
|
1554
|
+
{ field: 'file', type: 'file', required: false, default: '--', description: 'multipart/form-data: .pptx ファイル(最大25MB)。pptxBase64 とどちらか一方。' },
|
|
1555
|
+
{ field: 'pptxBase64', type: 'string', required: false, default: '--', description: 'application/json: base64エンコードした .pptx(data URL も可)。JSON-RPC / MCP クライアント向けに推奨。' },
|
|
1556
|
+
{ field: 'fileName', type: 'string', required: false, default: 'presentation.pptx', description: 'ラベル用の元ファイル名(任意)。' },
|
|
1557
|
+
],
|
|
1558
|
+
responseTitle: 'レスポンス (200 OK)',
|
|
1559
|
+
responseCode: '{\n "fileName": "deck.pptx",\n "plan": "api_starter",\n "slideCount": 13,\n "slides": [{ "index": 1, "html": "<section class=\\"slide\\">...</section>" }],\n "html": "<!doctype html>...",\n "css": "...",\n "warnings": []\n}',
|
|
1560
|
+
errors: [
|
|
1561
|
+
{ code: '400', description: 'ファイル欠如/不正、base64不正、または不正なOOXML。' },
|
|
1562
|
+
{ code: '401', description: 'APIキー未指定。Authorization: Bearer または X-API-Key を設定してください。' },
|
|
1563
|
+
{ code: '403', description: 'APIキーがAPIアクセス可能なプランに紐づいていません。' },
|
|
1564
|
+
{ code: '413', description: 'PPTXが25MBの上限を超えています。' },
|
|
1565
|
+
],
|
|
1566
|
+
},
|
|
1526
1567
|
{
|
|
1527
1568
|
method: 'POST',
|
|
1528
1569
|
path: '/api/export/jobs',
|
|
@@ -1892,6 +1933,7 @@ echo 'HTML2PPTX_API_KEY=sk_live_xxxx' >> .env`,
|
|
|
1892
1933
|
mcpTools: [
|
|
1893
1934
|
{ tool: 'html2pptx_list_export_plans', description: '現在の商用プラン一覧と推奨プランを取得。' },
|
|
1894
1935
|
{ tool: 'html2pptx_create_export_job', description: 'HTML/CSS からエクスポートジョブを作成。width、height、layout、waitForCompletion、timeoutMs、responseFormat ("url" | "base64" | "both") などのオプションにも対応。' },
|
|
1936
|
+
{ tool: 'html2pptx_import_pptx', description: 'エクスポートの逆方向。既存の .pptx を編集可能なHTMLスライドへ変換。pptxBase64 でファイルを渡す(リモートMCP対応)。ローカルstdio MCP では filePath も可。テーマ色・マスターのテキストスタイル・フォントを解決。' },
|
|
1895
1937
|
{ tool: 'html2pptx_get_export_job', description: 'jobId を指定してジョブの現在状態を取得。' },
|
|
1896
1938
|
{ tool: 'html2pptx_wait_for_export_job', description: '完了または失敗までジョブを自動ポーリング。内部でリトライとバックオフを処理。' },
|
|
1897
1939
|
{ tool: 'html2pptx_get_docs', description: 'html2pptx.app のドキュメントを取得。API契約、HTML要件、統合ガイドを理解するために使用。' },
|
|
@@ -112,6 +112,36 @@ export const TOOL_DEFINITIONS = [
|
|
|
112
112
|
openWorldHint: true,
|
|
113
113
|
},
|
|
114
114
|
},
|
|
115
|
+
{
|
|
116
|
+
name: 'html2pptx_import_pptx',
|
|
117
|
+
title: 'Import PPTX to HTML',
|
|
118
|
+
description:
|
|
119
|
+
'Convert an existing .pptx file into editable HTML slides (the reverse of export). Resolves theme colors (incl. clrMap/clrMapOvr inversion), master text styles, and font families. Provide the file as base64 in pptxBase64 (recommended, works on remote MCP). The local stdio MCP also accepts filePath to read a .pptx from disk. Returns full-deck html, per-slide html, and shared css.',
|
|
120
|
+
inputSchema: {
|
|
121
|
+
type: 'object',
|
|
122
|
+
properties: {
|
|
123
|
+
pptxBase64: {
|
|
124
|
+
type: 'string',
|
|
125
|
+
description: 'Base64-encoded .pptx contents. Data URLs (data:...;base64,) are also accepted.',
|
|
126
|
+
},
|
|
127
|
+
filePath: {
|
|
128
|
+
type: 'string',
|
|
129
|
+
description: 'Local filesystem path to a .pptx file. Only honored by the local stdio MCP server.',
|
|
130
|
+
},
|
|
131
|
+
fileName: {
|
|
132
|
+
type: 'string',
|
|
133
|
+
description: 'Optional original file name for labeling. Defaults to presentation.pptx.',
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
additionalProperties: false,
|
|
137
|
+
},
|
|
138
|
+
annotations: {
|
|
139
|
+
readOnlyHint: true,
|
|
140
|
+
destructiveHint: false,
|
|
141
|
+
idempotentHint: true,
|
|
142
|
+
openWorldHint: true,
|
|
143
|
+
},
|
|
144
|
+
},
|
|
115
145
|
{
|
|
116
146
|
name: 'html2pptx_get_export_job',
|
|
117
147
|
title: 'Get Export Job',
|
|
@@ -842,6 +872,17 @@ export async function executeTool(name, args, client, { sendNotification, progre
|
|
|
842
872
|
|
|
843
873
|
return buildToolResponse(renderJobText(result), result);
|
|
844
874
|
}
|
|
875
|
+
case 'html2pptx_import_pptx': {
|
|
876
|
+
if (typeof client.importPptx !== 'function') {
|
|
877
|
+
throw new Error('html2pptx_import_pptx is not available on this server.');
|
|
878
|
+
}
|
|
879
|
+
const data = await client.importPptx({
|
|
880
|
+
pptxBase64: typeof args.pptxBase64 === 'string' ? args.pptxBase64 : undefined,
|
|
881
|
+
filePath: typeof args.filePath === 'string' ? args.filePath : undefined,
|
|
882
|
+
fileName: typeof args.fileName === 'string' ? args.fileName : undefined,
|
|
883
|
+
});
|
|
884
|
+
return buildToolResponse(renderImportText(data), data);
|
|
885
|
+
}
|
|
845
886
|
case 'html2pptx_get_export_job': {
|
|
846
887
|
ensureNonEmptyString(args.jobId, 'jobId');
|
|
847
888
|
const responseFormat = resolveResponseFormat(args);
|
|
@@ -1423,6 +1464,19 @@ function renderUsageText(data) {
|
|
|
1423
1464
|
return lines.join('\n');
|
|
1424
1465
|
}
|
|
1425
1466
|
|
|
1467
|
+
function renderImportText(data) {
|
|
1468
|
+
const lines = [];
|
|
1469
|
+
const name = data?.fileName || 'presentation.pptx';
|
|
1470
|
+
lines.push(`Imported ${name}: ${data?.slideCount ?? 0} slide(s) converted to HTML.`);
|
|
1471
|
+
if (Array.isArray(data?.warnings) && data.warnings.length) {
|
|
1472
|
+
lines.push(`Warnings: ${data.warnings.length}`);
|
|
1473
|
+
for (const w of data.warnings.slice(0, 5)) lines.push(` - ${w}`);
|
|
1474
|
+
}
|
|
1475
|
+
const htmlLen = typeof data?.html === 'string' ? data.html.length : 0;
|
|
1476
|
+
lines.push(`HTML: ${htmlLen} bytes (full deck). Per-slide HTML available in slides[].html; shared styles in css.`);
|
|
1477
|
+
return lines.join('\n');
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1426
1480
|
function renderJobText(job) {
|
|
1427
1481
|
const lines = [];
|
|
1428
1482
|
lines.push(`Job ${job.jobId || 'unknown'} is ${job.status || 'unknown'}.`);
|
|
@@ -1895,6 +1895,7 @@ export default function EditSlideClient() {
|
|
|
1895
1895
|
const [inspectorTab, setInspectorTab] = useState('design');
|
|
1896
1896
|
const [selection, setSelection] = useState(null); // { selector, element }
|
|
1897
1897
|
const [expandedLayers, setExpandedLayers] = useState(() => new Set(['__root__']));
|
|
1898
|
+
const [leftTab, setLeftTab] = useState('slides');
|
|
1898
1899
|
const [filePath, setFilePath] = useState(null); // server-side relative path
|
|
1899
1900
|
const [assetScope, setAssetScope] = useState('project'); // project | global
|
|
1900
1901
|
const [saveStatus, setSaveStatus] = useState('idle'); // idle | saving | saved | error
|
|
@@ -3956,41 +3957,60 @@ ${slidesArg.join('\n\n')}
|
|
|
3956
3957
|
<div className="cv-main">
|
|
3957
3958
|
{/* ---------------- left panel (pages + layers) ---------------- */}
|
|
3958
3959
|
<aside className="cv-leftpanel">
|
|
3959
|
-
<div className="cv-lp-
|
|
3960
|
-
<
|
|
3961
|
-
|
|
3960
|
+
<div className="cv-lp-tabs" role="tablist">
|
|
3961
|
+
<button
|
|
3962
|
+
type="button"
|
|
3963
|
+
role="tab"
|
|
3964
|
+
aria-selected={leftTab === 'slides'}
|
|
3965
|
+
className={'cv-lp-tab' + (leftTab === 'slides' ? ' is-active' : '')}
|
|
3966
|
+
onClick={() => setLeftTab('slides')}
|
|
3967
|
+
>
|
|
3968
|
+
{text.slides}
|
|
3969
|
+
</button>
|
|
3970
|
+
<button
|
|
3971
|
+
type="button"
|
|
3972
|
+
role="tab"
|
|
3973
|
+
aria-selected={leftTab === 'layers'}
|
|
3974
|
+
className={'cv-lp-tab' + (leftTab === 'layers' ? ' is-active' : '')}
|
|
3975
|
+
onClick={() => setLeftTab('layers')}
|
|
3976
|
+
>
|
|
3977
|
+
{text.layers}
|
|
3978
|
+
</button>
|
|
3979
|
+
</div>
|
|
3980
|
+
{leftTab === 'slides' ? (
|
|
3981
|
+
<div className="cv-lp-section cv-lp-section--slides">
|
|
3982
|
+
<div className="cv-lp-pages">
|
|
3983
|
+
{slides.map((_, i) => (
|
|
3984
|
+
<button
|
|
3985
|
+
key={i}
|
|
3986
|
+
type="button"
|
|
3987
|
+
className={'cv-lp-page' + (i === active ? ' is-active' : '')}
|
|
3988
|
+
onClick={() => enterFocus(i)}
|
|
3989
|
+
>
|
|
3990
|
+
<span className="cv-lp-page__num">{i + 1}</span>
|
|
3991
|
+
<span className="cv-lp-page__thumb">
|
|
3992
|
+
{thumbs[i]
|
|
3993
|
+
? <img src={thumbs[i]} alt="" draggable={false} />
|
|
3994
|
+
: <span className="cv-lp-page__ph" />}
|
|
3995
|
+
</span>
|
|
3996
|
+
</button>
|
|
3997
|
+
))}
|
|
3998
|
+
</div>
|
|
3962
3999
|
</div>
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
: <span className="cv-lp-page__ph" />}
|
|
3976
|
-
</span>
|
|
3977
|
-
</button>
|
|
3978
|
-
))}
|
|
4000
|
+
) : (
|
|
4001
|
+
<div className="cv-lp-section cv-lp-section--layers">
|
|
4002
|
+
<LayerTree
|
|
4003
|
+
tree={layerTree}
|
|
4004
|
+
expandedKeys={expandedLayers}
|
|
4005
|
+
selectedKey={selectedLayerKey}
|
|
4006
|
+
onToggle={toggleLayer}
|
|
4007
|
+
onSelect={selectLayer}
|
|
4008
|
+
onPreview={previewLayer}
|
|
4009
|
+
onPreviewEnd={clearLayerPreview}
|
|
4010
|
+
labels={text.layer}
|
|
4011
|
+
/>
|
|
3979
4012
|
</div>
|
|
3980
|
-
|
|
3981
|
-
<div className="cv-lp-section cv-lp-section--layers">
|
|
3982
|
-
<div className="cv-lp-head"><span>{text.layers}</span></div>
|
|
3983
|
-
<LayerTree
|
|
3984
|
-
tree={layerTree}
|
|
3985
|
-
expandedKeys={expandedLayers}
|
|
3986
|
-
selectedKey={selectedLayerKey}
|
|
3987
|
-
onToggle={toggleLayer}
|
|
3988
|
-
onSelect={selectLayer}
|
|
3989
|
-
onPreview={previewLayer}
|
|
3990
|
-
onPreviewEnd={clearLayerPreview}
|
|
3991
|
-
labels={text.layer}
|
|
3992
|
-
/>
|
|
3993
|
-
</div>
|
|
4013
|
+
)}
|
|
3994
4014
|
</aside>
|
|
3995
4015
|
|
|
3996
4016
|
{/* ---------------- canvas viewport ---------------- */}
|
|
@@ -4400,11 +4420,16 @@ ${slidesArg.join('\n\n')}
|
|
|
4400
4420
|
.cv-main { display: flex; min-height: 0; min-width: 0; }
|
|
4401
4421
|
.cv-leftpanel { width: 280px; flex: 0 0 280px; display: flex; flex-direction: column; background: #ffffff; border-right: 1px solid #e6e6e6; overflow: hidden; }
|
|
4402
4422
|
.cv-lp-section { display: flex; flex-direction: column; border-bottom: 1px solid #ececec; }
|
|
4423
|
+
.cv-lp-section--slides { flex: 1 1 auto; min-height: 0; overflow: hidden; }
|
|
4424
|
+
.cv-lp-tabs { display: flex; gap: 2px; padding: 8px 8px 0; border-bottom: 1px solid #ececec; }
|
|
4425
|
+
.cv-lp-tab { flex: 1; height: 30px; font-size: 11px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; cursor: pointer; border: none; background: transparent; color: #8a8a8a; border-bottom: 2px solid transparent; }
|
|
4426
|
+
.cv-lp-tab:hover { color: #555; }
|
|
4427
|
+
.cv-lp-tab.is-active { color: #111; border-bottom-color: #0D99FF; }
|
|
4403
4428
|
.cv-lp-section--layers { flex: 1 1 auto; min-height: 0; overflow: hidden; }
|
|
4404
4429
|
.cv-lp-head { display: flex; align-items: center; justify-content: space-between; padding: 10px 12px 6px; font-size: 11px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: #8a8a8a; }
|
|
4405
4430
|
.cv-lp-add { width: 22px; height: 22px; display: grid; place-items: center; border: none; border-radius: 6px; background: transparent; color: #8a8a8a; cursor: pointer; }
|
|
4406
4431
|
.cv-lp-add:hover { background: #f0f0f0; color: #1a1a1a; }
|
|
4407
|
-
.cv-lp-pages { display: flex; flex-direction: column; gap: 8px; padding: 8px 10px 10px;
|
|
4432
|
+
.cv-lp-pages { display: flex; flex-direction: column; gap: 8px; padding: 8px 10px 10px; flex: 1 1 auto; min-height: 0; overflow-y: auto; }
|
|
4408
4433
|
.cv-lp-page { display: flex; align-items: center; gap: 8px; padding: 0; border: none; background: transparent; cursor: pointer; width: 100%; }
|
|
4409
4434
|
.cv-lp-page__num { flex: 0 0 auto; width: 14px; text-align: right; font-size: 10px; color: #9a9a9a; font-variant-numeric: tabular-nums; }
|
|
4410
4435
|
.cv-lp-page.is-active .cv-lp-page__num { color: #0a0a0a; font-weight: 700; }
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
normalizeEditPanelInputValue,
|
|
13
13
|
normalizeStyleValue,
|
|
14
14
|
} from '../../lib/edit-panel-value-normalizer';
|
|
15
|
+
import { keepTextEditable } from '../../lib/edit-slide-editor-helpers';
|
|
15
16
|
|
|
16
17
|
const TEXT_LEAF_TAGS = new Set([
|
|
17
18
|
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
@@ -265,7 +266,7 @@ export default function EditPropertyPanel({
|
|
|
265
266
|
|
|
266
267
|
const applyText = (value) => {
|
|
267
268
|
element.textContent = value;
|
|
268
|
-
setTextEdit(readTextEdit(element));
|
|
269
|
+
setTextEdit((prev) => keepTextEditable(readTextEdit(element), prev));
|
|
269
270
|
onChange?.();
|
|
270
271
|
};
|
|
271
272
|
|
|
@@ -273,7 +274,7 @@ export default function EditPropertyPanel({
|
|
|
273
274
|
const run = textEdit.runs[index];
|
|
274
275
|
if (!run?.node) return;
|
|
275
276
|
run.node.nodeValue = value;
|
|
276
|
-
setTextEdit(readTextEdit(element));
|
|
277
|
+
setTextEdit((prev) => keepTextEditable(readTextEdit(element), prev));
|
|
277
278
|
onChange?.();
|
|
278
279
|
};
|
|
279
280
|
|
|
@@ -686,36 +687,6 @@ function ImageAssetSection({ element, ui, assetTools, onChange }) {
|
|
|
686
687
|
</div>
|
|
687
688
|
) : null}
|
|
688
689
|
|
|
689
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8 }}>
|
|
690
|
-
<span style={{ fontSize: 11, color: '#9a9a9a' }}>{ui.saveTo}</span>
|
|
691
|
-
{[
|
|
692
|
-
['project', ui.assetScopeProject],
|
|
693
|
-
['global', ui.assetScopeGlobal],
|
|
694
|
-
].map(([value, label]) => (
|
|
695
|
-
<button
|
|
696
|
-
key={value}
|
|
697
|
-
type="button"
|
|
698
|
-
onClick={() => assetTools.setScope?.(value)}
|
|
699
|
-
style={{
|
|
700
|
-
flex: 1,
|
|
701
|
-
height: 26,
|
|
702
|
-
fontSize: 11.5,
|
|
703
|
-
fontWeight: 600,
|
|
704
|
-
cursor: 'pointer',
|
|
705
|
-
borderRadius: 6,
|
|
706
|
-
border: '1px solid ' + (scope === value ? '#0D99FF' : '#e0e0e0'),
|
|
707
|
-
background: scope === value ? 'rgba(13,153,255,0.12)' : '#fff',
|
|
708
|
-
color: scope === value ? '#0D78CC' : '#666',
|
|
709
|
-
}}
|
|
710
|
-
>
|
|
711
|
-
{label}
|
|
712
|
-
</button>
|
|
713
|
-
))}
|
|
714
|
-
</div>
|
|
715
|
-
|
|
716
|
-
<button type="button" className="ppt-lumina-add" disabled={busy} onClick={() => fileInputRef.current?.click()}>
|
|
717
|
-
{busy ? ui.uploading : isImg ? ui.replaceImage : isVideo ? ui.replaceVideo : ui.insertImage}
|
|
718
|
-
</button>
|
|
719
690
|
<input
|
|
720
691
|
ref={fileInputRef}
|
|
721
692
|
type="file"
|
|
@@ -731,67 +702,94 @@ function ImageAssetSection({ element, ui, assetTools, onChange }) {
|
|
|
731
702
|
<p style={{ margin: '6px 0 0', fontSize: 11, color: '#d23' }}>{error}</p>
|
|
732
703
|
) : null}
|
|
733
704
|
|
|
734
|
-
<div style={{ marginTop:
|
|
705
|
+
<div style={{ marginTop: 4 }}>
|
|
735
706
|
<h4 style={{ margin: '0 0 6px', fontSize: 11, fontWeight: 600, color: '#9a9a9a' }}>{ui.savedAssets}</h4>
|
|
736
|
-
{
|
|
737
|
-
<
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
{
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
)
|
|
771
|
-
|
|
707
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 6 }}>
|
|
708
|
+
<button
|
|
709
|
+
type="button"
|
|
710
|
+
disabled={busy}
|
|
711
|
+
onClick={() => fileInputRef.current?.click()}
|
|
712
|
+
title={ui.insertImage}
|
|
713
|
+
style={{
|
|
714
|
+
aspectRatio: '1 / 1',
|
|
715
|
+
display: 'flex',
|
|
716
|
+
alignItems: 'center',
|
|
717
|
+
justifyContent: 'center',
|
|
718
|
+
cursor: busy ? 'wait' : 'pointer',
|
|
719
|
+
borderRadius: 6,
|
|
720
|
+
border: '1px dashed #c8c8c8',
|
|
721
|
+
background: '#fafafa',
|
|
722
|
+
color: '#9a9a9a',
|
|
723
|
+
fontSize: 22,
|
|
724
|
+
lineHeight: 1,
|
|
725
|
+
}}
|
|
726
|
+
>
|
|
727
|
+
{busy ? '…' : '+'}
|
|
728
|
+
</button>
|
|
729
|
+
{assets.map((asset) => {
|
|
730
|
+
const kind = asset.kind || assetKindFromName(asset.src || asset.name);
|
|
731
|
+
const preview = assetTools.resolveAssetUrl?.(asset.src) || '';
|
|
732
|
+
const payload = JSON.stringify({ src: asset.src, name: asset.name, kind });
|
|
733
|
+
return (
|
|
734
|
+
<div key={asset.src} style={{ position: 'relative', minWidth: 0 }}>
|
|
735
|
+
<button
|
|
736
|
+
type="button"
|
|
737
|
+
title={`${asset.name} - ${ui.dragAssetHint}`}
|
|
738
|
+
draggable
|
|
739
|
+
onDragStart={(event) => {
|
|
740
|
+
event.dataTransfer.effectAllowed = 'copy';
|
|
741
|
+
event.dataTransfer.setData('application/x-edit-slide-asset', payload);
|
|
742
|
+
event.dataTransfer.setData('text/plain', asset.src);
|
|
743
|
+
}}
|
|
744
|
+
onClick={() => applyAssetSrc(asset.src, kind)}
|
|
745
|
+
style={{
|
|
746
|
+
padding: 0,
|
|
747
|
+
width: '100%',
|
|
748
|
+
aspectRatio: '1 / 1',
|
|
749
|
+
cursor: 'grab',
|
|
750
|
+
borderRadius: 6,
|
|
751
|
+
border: '1px solid #e6e6e6',
|
|
752
|
+
background: '#fafafa',
|
|
753
|
+
overflow: 'hidden',
|
|
754
|
+
display: 'block',
|
|
755
|
+
}}
|
|
756
|
+
>
|
|
757
|
+
{kind === 'video' ? (
|
|
758
|
+
<video src={preview} muted playsInline style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
|
759
|
+
) : (
|
|
760
|
+
<img src={preview} alt={asset.name} style={{ width: '100%', height: '100%', objectFit: 'contain' }} />
|
|
761
|
+
)}
|
|
762
|
+
</button>
|
|
763
|
+
{kind === 'image' ? (
|
|
772
764
|
<button
|
|
773
765
|
type="button"
|
|
774
766
|
onClick={() => applyAssetBackground(asset.src, kind)}
|
|
775
|
-
|
|
776
|
-
title={kind === 'image' ? ui.applyBackground : 'Video cannot be used as a CSS background'}
|
|
767
|
+
title={ui.applyBackground}
|
|
777
768
|
style={{
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
769
|
+
position: 'absolute',
|
|
770
|
+
right: 3,
|
|
771
|
+
bottom: 3,
|
|
772
|
+
padding: '1px 4px',
|
|
773
|
+
fontSize: 8,
|
|
774
|
+
fontWeight: 700,
|
|
775
|
+
lineHeight: 1.2,
|
|
776
|
+
color: '#fff',
|
|
777
|
+
background: 'rgba(0,0,0,0.55)',
|
|
778
|
+
border: 'none',
|
|
779
|
+
borderRadius: 4,
|
|
780
|
+
cursor: 'pointer',
|
|
786
781
|
}}
|
|
787
782
|
>
|
|
788
|
-
|
|
783
|
+
BG
|
|
789
784
|
</button>
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
785
|
+
) : null}
|
|
786
|
+
</div>
|
|
787
|
+
);
|
|
788
|
+
})}
|
|
789
|
+
</div>
|
|
790
|
+
{assets.length === 0 ? (
|
|
791
|
+
<p style={{ margin: '6px 0 0', fontSize: 11, color: '#b0b0b0' }}>{ui.noSavedAssets}</p>
|
|
792
|
+
) : null}
|
|
795
793
|
</div>
|
|
796
794
|
</>
|
|
797
795
|
)}
|
|
@@ -68,6 +68,18 @@ export function restorePreviewAssetUrls(root) {
|
|
|
68
68
|
root.querySelectorAll?.('img[src], video[src], source[src], [style]').forEach(restoreElementAssetUrls);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Clearing all text makes an element stop qualifying as a text element, so the
|
|
73
|
+
* panel's readTextEdit() returns mode 'none' and the Content editor unmounts —
|
|
74
|
+
* the now-empty element collapses and clicks fall through to a parent. While the
|
|
75
|
+
* user is actively editing, keep a plain (empty) editor so the selected element
|
|
76
|
+
* stays editable and selected.
|
|
77
|
+
*/
|
|
78
|
+
export function keepTextEditable(next, prev) {
|
|
79
|
+
if (next.mode !== 'none' || prev.mode === 'none') return next;
|
|
80
|
+
return { mode: 'plain', value: '', runs: [] };
|
|
81
|
+
}
|
|
82
|
+
|
|
71
83
|
export function isEditorTypingTarget(target) {
|
|
72
84
|
const element = target?.nodeType === 1 ? target : target?.parentElement;
|
|
73
85
|
if (!element) return false;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import process from 'node:process';
|
|
4
|
+
import { readFile } from 'node:fs/promises';
|
|
5
|
+
import { basename } from 'node:path';
|
|
4
6
|
import {
|
|
5
7
|
DEFAULT_PROTOCOL,
|
|
6
8
|
SERVER_INFO,
|
|
@@ -125,6 +127,24 @@ async function handleMessage(message) {
|
|
|
125
127
|
// or WorkOS JWT) so anonymous curl/browser can't scrape prompts.
|
|
126
128
|
requireApiKey: true,
|
|
127
129
|
}),
|
|
130
|
+
importPptx: async ({ pptxBase64, filePath, fileName } = {}) => {
|
|
131
|
+
let base64 = pptxBase64;
|
|
132
|
+
let name = fileName;
|
|
133
|
+
// The local stdio server can read a .pptx straight off disk.
|
|
134
|
+
if (!base64 && filePath) {
|
|
135
|
+
const data = await readFile(filePath);
|
|
136
|
+
base64 = data.toString('base64');
|
|
137
|
+
if (!name) name = basename(filePath);
|
|
138
|
+
}
|
|
139
|
+
if (!base64) {
|
|
140
|
+
throw new Error('html2pptx_import_pptx requires pptxBase64 or filePath.');
|
|
141
|
+
}
|
|
142
|
+
return requestJson('/api/v1/import/pptx', {
|
|
143
|
+
method: 'POST',
|
|
144
|
+
requireApiKey: true,
|
|
145
|
+
body: { pptxBase64: base64, fileName: name },
|
|
146
|
+
});
|
|
147
|
+
},
|
|
128
148
|
openLocalSlideEditor: async (args) => localSlideEditorManager.open(args),
|
|
129
149
|
stopLocalSlideEditor: async (sessionId) => localSlideEditorManager.stop(sessionId),
|
|
130
150
|
};
|