
Svelte 5で「2フェーズUX」を実装する — パラメータ入力と生成結果のプログレッシブ・ディスクロージャー
はじめに
AIツールのUIには共通の課題があります。入力フォームと生成結果を同時に表示すると、画面が情報過多になるのです。
テンプレートのパラメータを入力するフェーズと、AIの応答をストリーミングで確認するフェーズは、ユーザーの注意が向く先が異なります。この2つのフェーズをプログレッシブ・ディスクロージャー(段階的な情報開示)で切り替えるUIを実装しました。
前提・環境
- Svelte 5($stateルーン)
- SvelteKit
- TailwindCSS
2フェーズの定義

| フェーズ | ユーザーの行動 | 画面に表示するもの |
|---|---|---|
| 入力フェーズ | テンプレート選択、パラメータ入力 | 入力フォーム全体、説明文 |
| 生成フェーズ | ストリーミング結果を確認 | 入力の要約(折りたたみ)、生成結果 |
状態管理
Svelte 5の$stateで、各フェーズの表示状態を管理します。
// パラメータ入力の折りたたみ状態
let isParameterInputCollapsed = $state(false);
// 出力エリアの表示状態
let showOutput = $state(false);
// ストリーミング状態
let isLoading = $state(false);
let isStreaming = $state(false);
フェーズ切り替えのタイミング
「生成」ボタンが押されたときに、入力フェーズから生成フェーズに切り替えます。
async function generateOutput() {
if (!selectedTemplate) return;
// バリデーション
const missingRequired = Object.entries(selectedTemplate.parameters)
.filter(([key, param]) => param.required && !parameterValues[key])
.map(([key]) => key);
if (missingRequired.length > 0) {
// m はi18n(国際化)のメッセージ関数。SvelteKitのi18nライブラリ(Paraglideなど)が提供する。
// m.key_name() で現在のロケールに応じた翻訳文字列を返す。
// 例: m.please_fill_required() → "必須項目を入力してください" (ja) / "Please fill required fields" (en)
error = m.please_fill_required({ fields: missingRequired.join(', ') });
return;
}
// 出力をクリア
output = '';
error = null;
isLoading = true;
isStreaming = true;
// ここがフェーズ切り替え
isParameterInputCollapsed = true; // 入力を折りたたむ
showOutput = true; // 出力エリアを表示
// ページトップにスクロール
window.scrollTo({ top: 0, behavior: 'smooth' });
// ストリーミング開始...
}
重要な判断: 折りたたみは「生成ボタンクリック時」に行います。「最初のトークンが到着したとき」ではありません。ボタンを押した瞬間にUIが変化することで、「処理が始まった」というフィードバックをユーザーに即座に伝えます。
スクロール位置の管理
window.scrollTo({ top: 0, behavior: 'smooth' });
パラメータ入力が長い場合、ユーザーはページ下部にスクロールしていることがあります。折りたたみ後に出力エリアが見えない位置にあると、「何も起きていない」と感じてしまいます。scrollToでページトップに戻し、折りたたまれた入力サマリーと出力エリアの両方が視界に入るようにします。
折りたたみ時の入力サマリー
パラメータを折りたたんだとき、何を入力したか確認できないと不便です。$derivedで入力値のサマリーを動的に生成します。
$derivedはSvelte 5のリアクティブプリミティブで、他のリアクティブな値から派生する値を定義します。ReactのuseMemoに近い概念ですが、大きな違いがあります。useMemoでは依存配列を手動で指定する必要がありますが、$derivedではコンパイラが関数内で読み取られるリアクティブな値を解析し、依存関係を自動追跡します。
// Svelte 5 — 依存関係は自動追跡
let summary = $derived(() => parameterValues.name + selectedTemplate.title);
// React — 依存配列を手動で指定(漏れるとstale valueバグになる)
const summary = useMemo(() => parameterValues.name + selectedTemplate.title,
[parameterValues.name, selectedTemplate.title]);
なお、$derivedは純粋な計算のためのもので、副作用(fetch、DOM操作など)には$effectを使います。
| Svelte 5 | React | 用途 |
|---|---|---|
$state |
useState |
値を保持 |
$derived |
useMemo |
他の値から計算(依存関係は自動追跡) |
$effect |
useEffect |
副作用を実行 |
let parameterSummary = $derived(() => {
if (!selectedTemplate || !isParameterInputCollapsed) return '';
const filledParams = Object.entries(parameterValues)
// 入力済みのパラメータのみ残す(空白のみの値はtrim()で空文字になりfalsyとして除外)
.filter(([, value]) => value.trim())
.map(([key, value]) => {
// 長いテキストは50文字で切る
const truncatedValue = value.length > 50 ? value.slice(0, 50) + '...' : value;
return `${key}: ${truncatedValue}`;
})
.slice(0, 3); // 最大3パラメータまで表示
if (filledParams.length === 0) {
return m.no_parameters_set();
}
return filledParams.map(param => `• ${param}`).join('\n');
});
設計上の判断:
- 50文字で切る — 長いメールやメッセージを入力した場合、サマリーが画面を占領しないようにする
- 最大3パラメータ — すべてのパラメータを表示すると折りたたんだ意味がない
- 入力済みのみ表示 — 空のフィールドは表示しない
テンプレート表示のコンディショナルレンダリング
{#if !isParameterInputCollapsed}
<!-- 入力フェーズ:全パラメータのフォームを表示 -->
<div class="space-y-6 parameter-inputs">
{#if selectedTemplate}
{#each Object.entries(selectedTemplate.parameters) as [paramKey, param]}
<div class="magical-input-group">
<label for={paramKey}>
<span>{paramKey}</span>
{#if param.required}
<span class="magical-badge required-badge">{m.required()}</span>
{/if}
</label>
<p>{param.description}</p>
<!-- bind:value で双方向バインディング。
ユーザーの入力 → parameterValues[paramKey] に自動反映、
parameterValues[paramKey] の変更 → textarea の表示に自動反映。
Reactでは value + onChange の2つが必要だが、Svelteではbind:一つで済む。 -->
<textarea
bind:value={parameterValues[paramKey]}
placeholder={m.enter_parameter({ parameter: paramKey })}
required={param.required}
rows="4"
></textarea>
</div>
{/each}
{/if}
<!-- 生成ボタン -->
{#if selectedTemplate}
<button onclick={generateOutput} disabled={isLoading || !hasAllRequiredParams()}>
{#if isLoading}
{m.generating_magic()}
{:else}
{m.generate_output()}
{/if}
</button>
{/if}
</div>
{/if}
<!-- 生成フェーズ:出力を表示 -->
{#if showOutput}
<div class="output-section">
<OutputDisplay {output} {isLoading} {isStreaming} />
</div>
{/if}
isParameterInputCollapsedがtrueになると入力フォーム全体が非表示になり、代わりにサマリーが表示されます。showOutputがtrueになると出力エリアが表示されます。
折りたたみのトグル
ユーザーが折りたたみを展開して入力を修正できるようにします。
function toggleParameterInput() {
isParameterInputCollapsed = !isParameterInputCollapsed;
}
ヘッダー部分にChevronアイコンを配置し、クリックで展開/折りたたみを切り替えます。
テンプレート変更時のリセット
テンプレートを切り替えたとき、前回の折りたたみ状態をリセットします。
function handleTemplateSelect(template: Template) {
selectedTemplate = template;
parameterValues = Object.keys(template.parameters).reduce((acc, key) => {
acc[key] = '';
return acc;
}, {} as Record<string, string>);
error = null;
// 折りたたみ状態をリセット
isParameterInputCollapsed = false;
showOutput = false;
output = '';
}
新しいテンプレートを選んだときに前回の出力が残っていると混乱するため、すべてクリアします。
ボタン状態の細かい制御

生成ボタンは、認証状態と入力状態に応じて4つの表示を切り替えます。
<button
onclick={$authState.isAuthenticated ? generateOutput : handleUnauthenticatedGenerate}
disabled={isLoading || (!$authState.isAuthenticated ? false : !hasAllRequiredParams())}
>
{#if isLoading}
<div class="magical-loading mr-2"></div>
{m.generating_magic()}
{:else if !$authState.isAuthenticated}
{m.sign_in_to_generate()}
{:else if !hasAllRequiredParams()}
{m.complete_required_fields()}
{:else}
{m.generate_output()}
{/if}
</button>
| 状態 | テキスト | 動作 |
|---|---|---|
| 未認証 | "Sign in to Generate" | 認証UIにスクロール |
| 必須項目未入力 | "Complete Required Fields" | disabled |
| 準備完了 | "Generate Output" | 生成開始 |
| 生成中 | "Generating Magic..." | disabled + ローディング |
未認証時にボタンをdisabledにするのではなく、クリック可能にして認証UIへ誘導するのがポイントです。disabledボタンは「なぜ押せないのか」がわかりにくいため、クリック時にアクションで説明します。
function handleUnauthenticatedGenerate() {
const authSettingsElement = document.querySelector('.magical-auth-container');
if (authSettingsElement) {
authSettingsElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
// 一時的にハイライトして注意を引く
authSettingsElement.classList.add('auth-highlight');
setTimeout(() => {
authSettingsElement.classList.remove('auth-highlight');
}, 2000);
}
}
まとめ
AIツールの2フェーズUXの実装ポイント:
| 判断 | 選択 | 理由 |
|---|---|---|
| 折りたたみタイミング | ボタンクリック時 | 即時フィードバック |
| スクロール | トップに戻す | 出力エリアを確実に見せる |
| サマリー表示 | 最大3項目、50文字制限 | 折りたたみの意味を保つ |
| テンプレート変更時 | 全リセット | 前回の状態が残ると混乱 |
| 未認証ボタン | クリック可能 | 理由の説明をアクションで伝える |
小さなUXパターンですが、AIツールでは入力と出力のフェーズが明確に分かれるため、このパターンの効果が大きいです。




