HTML 之 ollama流式API客户端 -chat模式

环境变量设置如下:
set OLLAMA_HOST=0.0.0.0:8888
set OLLAMA_ORIGINS=*

ollama流式API客户端

<!DOCTYPE html>
<html lang="zh-CN">
	<head>
		<meta charset="UTF-8">
		<!-- 添加viewport标签确保移动端正确缩放 -->
		<meta name="viewport" content="width=device-width, initial-scale=1.0">
		<title>AI-chat模式</title>
		<style>
			body {
				font-family: 'Segoe UI', Arial, sans-serif;
				background: #f0f2f5;
				margin: 0;
				min-height: 100vh;
				/* 改为min-height避免内容溢出 */
				display: flex;
				justify-content: center;
				align-items: center;
			}

			.container {
				width: 95vw;
				/* 增加宽度占比 */
				height: 95vh;
				display: flex;
				flex-direction: column;
				gap: 10px;
				/* 缩小间隙 */
				padding: 5px;
				/* 添加内边距 */
			}

			#output {
				flex: 1;
				width: calc(100% - 10px);
				/* 考虑内边距 */
				min-height: 50%;
				padding: 12px;
				font-size: 16px;
				/* 增大字体 */
				border: 2px solid #e3e8ee;
				border-radius: 6px;
				background: white;
				resize: none;
				overflow-y: auto;
				/* 确保滚动条可用 */
			}

			.input-group {
				display: flex;
				flex-direction: column;
				/* 改为垂直布局 */
				gap: 8px;
				height: auto;
				/* 自动高度 */
			}

			#input {
				width: calc(100% - 10px);
				min-height: 80px;
				/* 更适合移动端的高度 */
				padding: 10px;
				font-size: 16px;
				border: 2px solid #e3e8ee;
				border-radius: 6px;
				resize: vertical;
				/* 允许垂直调整 */
			}

			button {
				padding: 12px 20px;
				background: #007bff;
				color: white;
				border: none;
				border-radius: 6px;
				font-size: 16px;
				cursor: pointer;
				transition: background 0.2s;
				touch-action: manipulation;
				/* 优化触摸响应 */
			}

			/* 新增按钮容器样式 */
			.button-row {
				display: flex;
				gap: 8px;
				width: 100%;
			}

			/* 发送按钮宽度设置 */
			button[onclick="sendMessage()"] {
				flex: 1;
				/* 占据剩余空间 */
				width: 80%;
			}

			button:active {
				background: #0056b3;
			}

			/* 新增图标按钮样式 */
			button.icon-button {
				padding: 12px;
				width: 20%;
				min-width: 60px;
				/* 防止过小 */
				/* 固定宽度 */
				background: #28a745;
				display: flex;
				justify-content: center;
				align-items: center;
			}

			/* 调整按钮组间距 */
			.button-group {
				display: flex;
				gap: 8px;
				margin-top: 8px;
			}

			.icon-button {
				position: relative;
			}

			/* 喇叭图标样式 */
			.icon-button svg {
				width: 24px;
				height: 24px;
				fill: white;
				transition: opacity 0.3s;
			}

			.icon-button .off-icon {
				position: absolute;
				opacity: 0;
			}

			/* 激活状态 */
			.icon-button.active .on-icon {
				opacity: 0;
			}

			.icon-button.active .off-icon {
				opacity: 1;
			}

			/* 颜色变化 */
			.icon-button.active {
				background: #dc3545;
			}

			/* 手机端响应式调整 */
			@media (max-width: 480px) {
				.container {
					width: 100vw;
					height: 100vh;
					padding: 5px;
				}

				#output {
					font-size: 15px;
					padding: 10px;
				}

				#input {
					font-size: 15px;
					min-height: 70px;
				}

				button {
					padding: 15px 20px;
					/* 增大点击区域 */
					font-size: 15px;
				}

				button.icon-button {
					padding: 10px;
					width: 44px;
				}

				.icon-button svg {
					width: 22px;
					height: 22px;
				}
			}
		</style>
	</head>
	<body>
		<div class="container">
			<textarea id="output" readonly placeholder="结果将显示在这里..."></textarea>
			<div class="input-group">
				<textarea id="input" rows="2" placeholder="输入命令(/clear 清空)Shift+Enter换行"></textarea>
				<div class="button-row">
					<button onclick="sendMessage()">发送</button>
					<button class="icon-button" onclick="playSound(this)">
						<svg class="on-icon" viewBox="0 0 24 24">
							<path fill="currentColor"
								d="M15 3v18l-5-4H4V7h6l5-4zm3.5 5.5c1-1 2.5-1.5 4-1.5v3c-.6 0-1.2.2-1.7.5l-2.3-2zm2.3 7.7c.8-.6 1.5-1.5 1.9-2.7h-3c-.1.5-.3 1-.7 1.4l1.8 1.3z" />
						</svg>

						<svg class="off-icon" viewBox="0 0 24 24">
							<path fill="currentColor"
								d="M15 3v18l-5-4H4V7h6l5-4zm7.1 14.7l-1.4-1.4-3.6-3.6-3.6 3.6-1.4-1.4 3.6-3.6-3.6-3.6 1.4-1.4 3.6 3.6 3.6-3.6 1.4 1.4-3.6 3.6 3.6 3.6z" />
						</svg>
					</button>
				</div>
			</div>
		</div>

		<script>
			const outputDiv = document.getElementById('output');

			let isSpeaking = false;
			let currentUtterance = null;
			let messages = [{
				role: "system",
				content: "You are a warm-hearted assistant, and you only speak Chinese."
			}];

			document.addEventListener("DOMContentLoaded", function() {
				document.addEventListener("keydown", function(event) {
					if (event.key === "Enter") {
						sendMessage();
						event.preventDefault();
					}
				});
			});

			function playSound(btn) {
				return new Promise((resolve) => { // 返回 Promise
					const content = outputDiv.value.split('AI:');
					if (!isSpeaking) {
						if (content) {
							// 创建语音实例
							currentUtterance = new SpeechSynthesisUtterance(content[content.length - 1]);
							currentUtterance.lang = 'zh-CN';

							// 语音结束回调
							currentUtterance.onend = () => {
								isSpeaking = false;
								btn.classList.toggle('active');
								resolve(); // 异步完成,通知外部
							};

							window.speechSynthesis.speak(currentUtterance);
							isSpeaking = true;
							btn.classList.toggle('active');
						} else {
							resolve(); // 无内容时直接 resolve
						}
					} else {
						window.speechSynthesis.cancel();
						isSpeaking = false;
						btn.classList.toggle('active');
						resolve(); // 异步完成,通知外部
					}
				});
			}

			// 添加 HTML 转义函数
			const sanitizeHTML = (str) => {
				const div = document.createElement('div');
				div.textContent = str.replace(/\s/g, '');
				return div.innerHTML;
			};

			function sendMessage() {
				const input = document.getElementById('input').value;
				document.getElementById('input').value = '';
				if (outputDiv.value.trim()) {
					const content = outputDiv.value.split('AI:');
					messages.push({
						role: "assistant",
						content: content[content.length - 1]
					});
					if (messages.length > 5) messages.splice(1, 2);
				}
				outputDiv.value += `\n\n您:${sanitizeHTML(input)}\n\nAI:\n`;
				outputDiv.scrollTop = outputDiv.scrollHeight;
				if (input.trim() === '/clear') {
					outputDiv.value = '';
					return;
				}

				const url = "http://192.168.0.223:8888/api/chat";
				messages.push({
					role: "user",
					content: input
				});
				const data = {
					model: "deepseek-r1:8b",
					messages: messages,
					stream: true
				};

				fetch(url, {
						method: 'POST',
						headers: {
							'Content-Type': 'application/json'
						},
						body: JSON.stringify(data),
						mode: 'cors'
					})
					.then(response => {
						if (!response.ok) throw new Error(`HTTP错误: ${response.status}`);

						const reader = response.body.getReader();
						const decoder = new TextDecoder('utf-8');
						let prevChunk = '';

						return new ReadableStream({
							start(controller) {
								function pushChunk() {
									reader.read().then(({
										done,
										value
									}) => {
										if (done) {
											controller.close();
											return;
										}

										const chunk = decoder.decode(value, {
											stream: true
										});
										const combined = prevChunk + chunk;
										const split = combined.split('\n');
										prevChunk = split.pop() || '';

										split.forEach(line => {
											try {
												const parsed = JSON.parse(line);
												const text = parsed.message.content;
												outputDiv.value += text; // 将结果追加到文本框
												// 滚动到底部
												outputDiv.scrollTop = outputDiv.scrollHeight;
											} catch (e) {
												console.error('解析错误:', e);
											}
										});

										controller.enqueue(value);
										pushChunk();
									});
								}
								pushChunk();
							}
						});
					})
					.then(() => console.log('流式处理完成'))
					.catch(error => {
						outputDiv.value += `错误:${error.message}\n`; // 错误信息也显示在文本框
					});
			}
		</script>
	</body>
</html>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值