Skip to content

Commit 2cbc4bf

Browse files
authored
Merge pull request #181 from igapyon/tiga0310hbw
prompt-gen の可変入力対応と入力サニタイズを追加
2 parents bdb61bd + 0f17f10 commit 2cbc4bf

9 files changed

Lines changed: 351 additions & 33 deletions

File tree

docs/prompt/README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,18 @@
5050
- `P` 系列:
5151
- 世間で広く使われている、普及度の高い定番プロンプトを整理する系列です。
5252

53+
### 図解化系列の使い分け
54+
55+
- `S603-5xx`:
56+
- Mermaid を使った図解化を、生成AI視点の派生提案として扱う系列です。
57+
- `Mermaid timeline``Mermaid flowchart``Mermaid mindmap` など、構造化テキストから図へ落とし込みやすい表現を提案する用途を想定します。
58+
- `P973` 以降:
59+
- 図解化を、世間的にも再利用しやすい定番プロンプトとして整理する系列です。
60+
- `図解化テキスト化``図解化描画指示``概念マップ化``タイムライン化``フローチャート化``マインドマップ化` などを含みます。
61+
- 使い分けの目安:
62+
- AIに「こういう派生もありそう」と提案させたい場合は `S603-5xx`
63+
- 定番候補として安定運用したい場合は `P973` 以降
64+
5365
## ビルド
5466

5567
- `npm run build:prompt`
@@ -75,6 +87,16 @@
7587
- ハンバーガーメニューから `A / X / S / P` 系列の表示有無を切り替えられ、その設定は `localStorage` に保存される
7688
- 検索は空欄で全候補表示、空白区切りで AND、各語の照合先は label / keywords / かな・カナ・ローマ字展開を含む
7789

90+
### 入力値制約
91+
92+
- `commitId``subject` のような可変入力は、そのまま本文へ埋め込まず、サニタイズ後の値をバッククオートで囲って返す
93+
- 入力値は 1024 文字で打ち切り、以降は切り捨てる
94+
- 制御文字、改行、タブなどは半角スペース 1 個へ変換する
95+
- バッククオートはシングルクオートへ変換する
96+
- これらは prompt injection への最低限の緩和策であり、完全な防御ではない
97+
- `commitId` の文字種制限や、`subject` を命令ではなく文字列値として扱う明示文は、現時点では未実装で TODO 管理とする
98+
- これは prompt injection リスクの軽減を目的とした最低限の制約であり、完全な防御ではない
99+
78100
## 検索とキーワード
79101

80102
- 検索ボックスの挙動と `keywords` の設計方針は [prompt-gen 検索とキーワード設計](./prompt-gen-search-and-keywords.md) を参照する

docs/prompt/TODO.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
- [x] S603-002: 現在の会話の論点遷移を Mermaid で記述(AI提案)
2323
- [x] S603-003: 現在の会話の依存関係を Mermaid で記述(AI提案)
2424
- [ ] S603-001 は A603 との対応が弱いため、別の枝番へ移設する
25+
- [ ] prompt-gen の可変入力で、`commitId` は SHA らしい文字だけを許可するか検討する
26+
- [ ] prompt-gen の可変入力で、`subject` は命令ではなく文字列値として扱う文言を本文へ追加するか検討する
2527
- [x] S603-501: Mermaid timeline で図解化(AI提案)
2628
- [x] S603-502: Mermaid flowchart で図解化(AI提案)
2729
- [x] S603-503: Mermaid mindmap で図解化(AI提案)

docs/prompt/prompt-gen-src.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,18 @@
9191
</div>
9292
</section>
9393

94+
<section id="subjectInputSection" class="md-section md-stack-md md-hidden">
95+
<div class="md-form-grid md-form-grid-2">
96+
<lht-text-field-help
97+
field-id="subjectInput"
98+
label="subject"
99+
required
100+
placeholder="例: a small fox"
101+
help-text="プロンプト内の [subject] に差し込む対象を入力します。英語でも日本語でも構いません。">
102+
</lht-text-field-help>
103+
</div>
104+
</section>
105+
94106
<section id="promptOutputSection" class="md-section md-stack-md md-hidden">
95107
<div class="md-output-header">
96108
<p id="promptOutputTitle" class="md-output-title">生成結果</p>

docs/prompt/prompt-gen.html

Lines changed: 75 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1383,6 +1383,18 @@
13831383
</div>
13841384
</section>
13851385

1386+
<section id="subjectInputSection" class="md-section md-stack-md md-hidden">
1387+
<div class="md-form-grid md-form-grid-2">
1388+
<lht-text-field-help
1389+
field-id="subjectInput"
1390+
label="subject"
1391+
required
1392+
placeholder="例: a small fox"
1393+
help-text="プロンプト内の [subject] に差し込む対象を入力します。英語でも日本語でも構いません。">
1394+
</lht-text-field-help>
1395+
</div>
1396+
</section>
1397+
13861398
<section id="promptOutputSection" class="md-section md-stack-md md-hidden">
13871399
<div class="md-output-header">
13881400
<p id="promptOutputTitle" class="md-output-title">生成結果</p>
@@ -4156,6 +4168,24 @@
41564168
ちなみに、この楽譜編集アプリの頑張りポイントは、MusicXML を極力壊さずに編集するという点です。DOMツリーまま おそるおそる内容を変更するという編集仕草をチャッピー (ChatGPT)に徹底的に頑張って作ってもらいました。
41574169
\`\`\``
41584170
},
4171+
{
4172+
id: "washi-collage-whisper-request",
4173+
label: "852: 和紙切絵作品",
4174+
keywords: ["washi collage whisper", "washi collage", "torn paper collage", "paper collage", "kozo paper", "handmade paper", "cute illustration", "image prompt", "illustration prompt", "和紙", "和紙コラージュ", "ちぎり絵", "手漉き和紙", "楮紙", "かわいいイラスト", "画像生成", "画像プロンプト", "いらすと", "わし", "ちぎりえ", "てすきわし"],
4175+
requiresCommitId: false,
4176+
requiresSubject: true,
4177+
buildBody: (_commitId, subject) => {
4178+
const normalizedSubject = String(subject || "").trim();
4179+
if (!normalizedSubject) {
4180+
return "";
4181+
}
4182+
return `# Washi Collage Whisper v20250501a
4183+
4184+
A simplified cute illustration of ${normalizedSubject}, created by assembling a minimal number of large torn pieces of rich, muted pastel-colored handmade hand-scooped kozo paper (traditional Japanese mulberry paper). Each piece is separated by bold, rough torn edges with thick, visible, randomly oriented white fibers. The edges are soft, fuzzy, and deeply frayed, showing natural irregularities. Fibers vary in thickness-from very fine to slightly coarse-and in density and direction, creating an organic, handcrafted feel; some areas display extra-frayed, protruding fibers. Fiber color subtly shifts from pure white to off-white and warm beige.
4185+
The paper pieces are tinted in deep, smoky pastel tones-darker and thicker than ordinary pastels-avoiding vivid hues yet holding enough chroma to stay vibrant and prevent washed-out effects against the translucent handmade paper.
4186+
The overall texture is soft and fibrous, with bleeding fiber patterns radiating irregularly from the torn edges. Rendered in a flat 2-D style inspired by traditional Japanese torn-paper collage art, it features expansive colour fields, natural irregular edges, a warm and friendly mood, a simple unobtrusive background, and absolutely no strong shadows or dramatic lighting.`;
4187+
}
4188+
},
41594189
{
41604190
id: "chat-partner-mikuku-request",
41614191
label: "801: 会話相手: みくく",
@@ -6666,13 +6696,15 @@
66666696
const promptSearch = document.getElementById("promptSearch");
66676697
const promptCandidateArea = document.getElementById("promptCandidateArea");
66686698
const commitInputSection = document.getElementById("commitInputSection");
6699+
const subjectInputSection = document.getElementById("subjectInputSection");
66696700
const promptOutputSection = document.getElementById("promptOutputSection");
66706701
const promptOutputTitle = document.getElementById("promptOutputTitle");
66716702
const promptOutputHelp = document.getElementById("promptOutputHelp");
66726703
const commitIdInput = document.getElementById("commitId");
6704+
const subjectInput = document.getElementById("subjectInput");
66736705
const includeLabelPrefix = document.getElementById("includeLabelPrefix");
66746706
const promptOutput = document.getElementById("promptOutput");
6675-
if (!promptSearch || !commitIdInput || !includeLabelPrefix || !promptOutput || !promptCandidateArea || !commitInputSection || !promptOutputSection || !promptOutputTitle || !promptOutputHelp) {
6707+
if (!promptSearch || !commitIdInput || !subjectInput || !includeLabelPrefix || !promptOutput || !promptCandidateArea || !commitInputSection || !subjectInputSection || !promptOutputSection || !promptOutputTitle || !promptOutputHelp) {
66766708
return;
66776709
}
66786710
function loadSeriesVisibilitySettings() {
@@ -6736,6 +6768,33 @@
67366768
}
67376769
applyLatinInputHints(promptSearch, "search");
67386770
applyLatinInputHints(commitIdInput, "done");
6771+
const MAX_EMBED_INPUT_LENGTH = 1024;
6772+
function sanitizePromptVariableInput(value) {
6773+
const normalizedWhitespace = String(value || "")
6774+
.replace(/[\u0000-\u001F\u007F-\u009F]/g, " ")
6775+
.replace(/[\u200B-\u200D\u2060\uFEFF]/g, " ");
6776+
const trimmedValue = normalizedWhitespace.trim();
6777+
if (!trimmedValue) {
6778+
return "";
6779+
}
6780+
const truncatedValue = trimmedValue.slice(0, MAX_EMBED_INPUT_LENGTH);
6781+
const normalizedQuotes = truncatedValue.replace(/`/g, "'");
6782+
return `\`${normalizedQuotes}\``;
6783+
}
6784+
function syncInputSections(selectedDefinition) {
6785+
if (selectedDefinition === null || selectedDefinition === void 0 ? void 0 : selectedDefinition.requiresCommitId) {
6786+
commitInputSection.classList.remove("md-hidden");
6787+
}
6788+
else {
6789+
commitInputSection.classList.add("md-hidden");
6790+
}
6791+
if (selectedDefinition === null || selectedDefinition === void 0 ? void 0 : selectedDefinition.requiresSubject) {
6792+
subjectInputSection.classList.remove("md-hidden");
6793+
}
6794+
else {
6795+
subjectInputSection.classList.add("md-hidden");
6796+
}
6797+
}
67396798
let selectedPrompt = "";
67406799
let lastSearchQuery = "";
67416800
const kanaToRomajiMap = new Map([
@@ -7109,9 +7168,10 @@
71097168
promptCandidateArea.innerHTML = "";
71107169
if (queryChanged) {
71117170
selectedPrompt = "";
7112-
commitInputSection.classList.add("md-hidden");
7171+
syncInputSections(null);
71137172
promptOutputSection.classList.add("md-hidden");
71147173
commitIdInput.value = "";
7174+
subjectInput.value = "";
71157175
promptOutput.textContent = "";
71167176
}
71177177
const { matchedDefinitions, prioritizedSingleDefinition } = searchPromptDefinitions(query, promptSearchIndexes, promptSearchIndexById);
@@ -7141,10 +7201,13 @@
71417201
if (prioritizedSingleDefinition && selectedPrompt === prioritizedSingleDefinition.id) {
71427202
const selectedDefinition = prioritizedSingleDefinition;
71437203
if (selectedDefinition.requiresCommitId) {
7144-
commitInputSection.classList.remove("md-hidden");
7204+
syncInputSections(selectedDefinition);
7205+
}
7206+
else if (selectedDefinition.requiresSubject) {
7207+
syncInputSections(selectedDefinition);
71457208
}
71467209
else {
7147-
commitInputSection.classList.add("md-hidden");
7210+
syncInputSections(selectedDefinition);
71487211
}
71497212
revealOutputSection({
71507213
scrollIntoView: queryChanged && query.length > 0
@@ -7160,15 +7223,15 @@
71607223
button.classList.add("is-active");
71617224
revealOutputSection({ scrollIntoView: true });
71627225
const selectedDefinition = visiblePromptDefinitions.find((definition) => definition.id === id);
7226+
syncInputSections(selectedDefinition || null);
71637227
if (selectedDefinition === null || selectedDefinition === void 0 ? void 0 : selectedDefinition.requiresCommitId) {
7164-
commitInputSection.classList.remove("md-hidden");
71657228
commitIdInput.focus();
71667229
}
7167-
else {
7168-
commitInputSection.classList.add("md-hidden");
7230+
else if (selectedDefinition === null || selectedDefinition === void 0 ? void 0 : selectedDefinition.requiresSubject) {
7231+
subjectInput.focus();
71697232
}
71707233
updateOutput();
7171-
if ((options === null || options === void 0 ? void 0 : options.autoCopy) && selectedDefinition && !selectedDefinition.requiresCommitId) {
7234+
if ((options === null || options === void 0 ? void 0 : options.autoCopy) && selectedDefinition && !selectedDefinition.requiresCommitId && !selectedDefinition.requiresSubject) {
71727235
await copyText(promptOutput.textContent || "");
71737236
}
71747237
}
@@ -7178,8 +7241,9 @@
71787241
return "";
71797242
}
71807243
const label = selectedDefinition ? selectedDefinition.label : "";
7181-
const commitId = (commitIdInput.value || "").trim();
7182-
const body = selectedDefinition ? selectedDefinition.buildBody(commitId) : "";
7244+
const commitId = sanitizePromptVariableInput(commitIdInput.value || "");
7245+
const subject = sanitizePromptVariableInput(subjectInput.value || "");
7246+
const body = selectedDefinition ? selectedDefinition.buildBody(commitId, subject) : "";
71837247
if (!body || !label) {
71847248
return "";
71857249
}
@@ -7257,6 +7321,7 @@
72577321
}
72587322
promptSearch.addEventListener("input", renderCandidates);
72597323
commitIdInput.addEventListener("input", updateOutput);
7324+
subjectInput.addEventListener("input", updateOutput);
72607325
includeLabelPrefix.addEventListener("change", updateOutput);
72617326
createSeriesVisibilityMenu();
72627327
renderCandidates();

docs/prompt/src/prompt-gen/js/main.js

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,15 @@ async function initializePromptPage() {
3737
const promptSearch = document.getElementById("promptSearch");
3838
const promptCandidateArea = document.getElementById("promptCandidateArea");
3939
const commitInputSection = document.getElementById("commitInputSection");
40+
const subjectInputSection = document.getElementById("subjectInputSection");
4041
const promptOutputSection = document.getElementById("promptOutputSection");
4142
const promptOutputTitle = document.getElementById("promptOutputTitle");
4243
const promptOutputHelp = document.getElementById("promptOutputHelp");
4344
const commitIdInput = document.getElementById("commitId");
45+
const subjectInput = document.getElementById("subjectInput");
4446
const includeLabelPrefix = document.getElementById("includeLabelPrefix");
4547
const promptOutput = document.getElementById("promptOutput");
46-
if (!promptSearch || !commitIdInput || !includeLabelPrefix || !promptOutput || !promptCandidateArea || !commitInputSection || !promptOutputSection || !promptOutputTitle || !promptOutputHelp) {
48+
if (!promptSearch || !commitIdInput || !subjectInput || !includeLabelPrefix || !promptOutput || !promptCandidateArea || !commitInputSection || !subjectInputSection || !promptOutputSection || !promptOutputTitle || !promptOutputHelp) {
4749
return;
4850
}
4951
function loadSeriesVisibilitySettings() {
@@ -107,6 +109,33 @@ async function initializePromptPage() {
107109
}
108110
applyLatinInputHints(promptSearch, "search");
109111
applyLatinInputHints(commitIdInput, "done");
112+
const MAX_EMBED_INPUT_LENGTH = 1024;
113+
function sanitizePromptVariableInput(value) {
114+
const normalizedWhitespace = String(value || "")
115+
.replace(/[\u0000-\u001F\u007F-\u009F]/g, " ")
116+
.replace(/[\u200B-\u200D\u2060\uFEFF]/g, " ");
117+
const trimmedValue = normalizedWhitespace.trim();
118+
if (!trimmedValue) {
119+
return "";
120+
}
121+
const truncatedValue = trimmedValue.slice(0, MAX_EMBED_INPUT_LENGTH);
122+
const normalizedQuotes = truncatedValue.replace(/`/g, "'");
123+
return `\`${normalizedQuotes}\``;
124+
}
125+
function syncInputSections(selectedDefinition) {
126+
if (selectedDefinition === null || selectedDefinition === void 0 ? void 0 : selectedDefinition.requiresCommitId) {
127+
commitInputSection.classList.remove("md-hidden");
128+
}
129+
else {
130+
commitInputSection.classList.add("md-hidden");
131+
}
132+
if (selectedDefinition === null || selectedDefinition === void 0 ? void 0 : selectedDefinition.requiresSubject) {
133+
subjectInputSection.classList.remove("md-hidden");
134+
}
135+
else {
136+
subjectInputSection.classList.add("md-hidden");
137+
}
138+
}
110139
let selectedPrompt = "";
111140
let lastSearchQuery = "";
112141
const kanaToRomajiMap = new Map([
@@ -480,9 +509,10 @@ async function initializePromptPage() {
480509
promptCandidateArea.innerHTML = "";
481510
if (queryChanged) {
482511
selectedPrompt = "";
483-
commitInputSection.classList.add("md-hidden");
512+
syncInputSections(null);
484513
promptOutputSection.classList.add("md-hidden");
485514
commitIdInput.value = "";
515+
subjectInput.value = "";
486516
promptOutput.textContent = "";
487517
}
488518
const { matchedDefinitions, prioritizedSingleDefinition } = searchPromptDefinitions(query, promptSearchIndexes, promptSearchIndexById);
@@ -512,10 +542,13 @@ async function initializePromptPage() {
512542
if (prioritizedSingleDefinition && selectedPrompt === prioritizedSingleDefinition.id) {
513543
const selectedDefinition = prioritizedSingleDefinition;
514544
if (selectedDefinition.requiresCommitId) {
515-
commitInputSection.classList.remove("md-hidden");
545+
syncInputSections(selectedDefinition);
546+
}
547+
else if (selectedDefinition.requiresSubject) {
548+
syncInputSections(selectedDefinition);
516549
}
517550
else {
518-
commitInputSection.classList.add("md-hidden");
551+
syncInputSections(selectedDefinition);
519552
}
520553
revealOutputSection({
521554
scrollIntoView: queryChanged && query.length > 0
@@ -531,15 +564,15 @@ async function initializePromptPage() {
531564
button.classList.add("is-active");
532565
revealOutputSection({ scrollIntoView: true });
533566
const selectedDefinition = visiblePromptDefinitions.find((definition) => definition.id === id);
567+
syncInputSections(selectedDefinition || null);
534568
if (selectedDefinition === null || selectedDefinition === void 0 ? void 0 : selectedDefinition.requiresCommitId) {
535-
commitInputSection.classList.remove("md-hidden");
536569
commitIdInput.focus();
537570
}
538-
else {
539-
commitInputSection.classList.add("md-hidden");
571+
else if (selectedDefinition === null || selectedDefinition === void 0 ? void 0 : selectedDefinition.requiresSubject) {
572+
subjectInput.focus();
540573
}
541574
updateOutput();
542-
if ((options === null || options === void 0 ? void 0 : options.autoCopy) && selectedDefinition && !selectedDefinition.requiresCommitId) {
575+
if ((options === null || options === void 0 ? void 0 : options.autoCopy) && selectedDefinition && !selectedDefinition.requiresCommitId && !selectedDefinition.requiresSubject) {
543576
await copyText(promptOutput.textContent || "");
544577
}
545578
}
@@ -549,8 +582,9 @@ async function initializePromptPage() {
549582
return "";
550583
}
551584
const label = selectedDefinition ? selectedDefinition.label : "";
552-
const commitId = (commitIdInput.value || "").trim();
553-
const body = selectedDefinition ? selectedDefinition.buildBody(commitId) : "";
585+
const commitId = sanitizePromptVariableInput(commitIdInput.value || "");
586+
const subject = sanitizePromptVariableInput(subjectInput.value || "");
587+
const body = selectedDefinition ? selectedDefinition.buildBody(commitId, subject) : "";
554588
if (!body || !label) {
555589
return "";
556590
}
@@ -628,6 +662,7 @@ async function initializePromptPage() {
628662
}
629663
promptSearch.addEventListener("input", renderCandidates);
630664
commitIdInput.addEventListener("input", updateOutput);
665+
subjectInput.addEventListener("input", updateOutput);
631666
includeLabelPrefix.addEventListener("change", updateOutput);
632667
createSeriesVisibilityMenu();
633668
renderCandidates();

0 commit comments

Comments
 (0)