Skip to content

Instantly share code, notes, and snippets.

@trtd56
Last active April 14, 2025 14:04
Show Gist options
  • Save trtd56/fe19ecb333a3b5d9a465e39783e3c0ce to your computer and use it in GitHub Desktop.
Save trtd56/fe19ecb333a3b5d9a465e39783e3c0ce to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>APIストリーミング表示</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
/* Interフォントを適用 */
body {
font-family: 'Inter', sans-serif;
}
/* スクロール可能な出力エリア */
#output {
height: 400px; /* 高さを固定 */
overflow-y: auto; /* 縦方向のスクロールを有効化 */
border: 1px solid #e5e7eb; /* 境界線 */
padding: 1rem; /* 内側余白 */
border-radius: 0.375rem; /* 角丸 */
background-color: #f9fafb; /* 背景色 */
white-space: pre-wrap; /* 改行とスペースを保持 */
word-wrap: break-word; /* 長い単語を折り返す */
}
</style>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
</head>
<body class="bg-gray-100 p-8">
<div class="max-w-2xl mx-auto bg-white p-6 rounded-lg shadow-md">
<h1 class="text-2xl font-bold mb-4 text-center text-gray-800">dLLM Demo</h1>
<div class="mb-4">
<label for="apiKey" class="block text-sm font-medium text-gray-700 mb-1">APIキー:</label>
<input type="password" id="apiKey" name="apiKey" placeholder="ここにAPIキーを入力してください" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500">
<p class="text-xs text-gray-500 mt-1">APIキーはこのブラウザ内でのみ使用され、外部には送信されません。</p>
</div>
<div class="mb-4">
<label for="question" class="block text-sm font-medium text-gray-700 mb-1">質問:</label>
<input type="text" id="question" name="question" value="" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500">
</div>
<button id="startButton" class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition duration-150 ease-in-out disabled:opacity-50">
生成
</button>
<div id="loading" class="mt-4 text-center text-gray-600" style="display: none;">
<svg class="animate-spin h-5 w-5 mr-3 inline-block text-indigo-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
読み込み中...
</div>
<div id="error" class="mt-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded-md" style="display: none;"></div>
<h2 class="text-xl font-semibold mt-6 mb-2 text-gray-700">応答:</h2>
<div id="output" class="whitespace-pre-wrap break-words text-gray-800"></div>
</div>
<script>
const startButton = document.getElementById('startButton');
const outputDiv = document.getElementById('output');
const apiKeyInput = document.getElementById('apiKey');
const questionInput = document.getElementById('question');
const loadingDiv = document.getElementById('loading');
const errorDiv = document.getElementById('error');
// 1秒待機する非同期関数
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
startButton.addEventListener('click', async () => {
const apiKey = apiKeyInput.value.trim();
const question = questionInput.value.trim();
const apiUrl = 'https://api.inceptionlabs.ai/v1/chat/completions';
if (!apiKey) {
showError('APIキーを入力してください。');
return;
}
if (!question) {
showError('質問を入力してください。');
return;
}
// UIリセット
outputDiv.textContent = ''; // 出力エリアをクリア
loadingDiv.style.display = 'block'; // ローディング表示
errorDiv.style.display = 'none'; // エラー非表示
startButton.disabled = true; // ボタンを無効化
// *** 変更点: 全文保持変数は不要になったため削除 ***
// let fullResponseText = '';
try {
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}` // APIキーを設定
},
body: JSON.stringify({
model: "mercury-coder-small", // モデル名
messages: [
{ "role": "user", "content": question } // ユーザーの質問
],
max_tokens: 200,
stream: true,
diffusing: true
})
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({ message: `HTTPエラー: ${response.status}` }));
throw new Error(errorData.message || `HTTPエラー: ${response.status}`);
}
if (!response.body) {
throw new Error('レスポンスボディがありません。');
}
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
while (true) {
const { value, done } = await reader.read();
if (done) {
console.log('ストリームが完了しました。');
break;
}
const lines = value.split('\n');
let chunkContent = ''; // 現在のチャンクで受信した内容を保持
lines.forEach(line => {
if (line.startsWith('data: ')) {
try {
const jsonStr = line.substring(6).trim();
if (jsonStr && jsonStr !== '[DONE]') {
const jsonData = JSON.parse(jsonStr);
const content = jsonData?.choices?.[0]?.delta?.content ?? '';
if (content) {
chunkContent += content; // チャンクの内容を一時変数に追加
}
}
} catch (e) {
console.error('JSONのパースに失敗:', line, e);
}
}
});
// *** 変更点: 現在のチャンク内容のみを表示 ***
if (chunkContent) {
// fullResponseText += chunkContent; // 全文への追加はしない
outputDiv.textContent = chunkContent; // 現在のチャンクの内容で表示を上書き
outputDiv.scrollTop = outputDiv.scrollHeight; // 自動スクロール
} else {
// 内容が空のチャンクが来た場合、表示をクリアする(必要に応じて)
// outputDiv.textContent = '';
}
// 各チャンク処理後に1秒待機
await sleep(1000);
}
} catch (error) {
console.error('ストリーミングエラー:', error);
showError(`エラーが発生しました: ${error.message}`);
} finally {
loadingDiv.style.display = 'none';
startButton.disabled = false;
}
});
// エラーメッセージ表示関数
function showError(message) {
errorDiv.textContent = message;
errorDiv.style.display = 'block';
loadingDiv.style.display = 'none';
startButton.disabled = false;
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment