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

环境变量设置如下:
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-generate模式</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;

			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 = '';
				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/generate";
				const data = {
					model: "deepseek-r1:8b",
					prompt: input
				};

				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.response;
												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>

### Ollama 流式输出实现 在 LangChain 中,所有 `Runnable` 对象支持同步方法 `stream` 和异步方法 `astream` 来逐块流式传输输出[^1]。这意味着每个组件能够按块处理输入并生成对应的输出。 对于 Ollama流式输出配置,由于其暴露的接口默认为流式输出模式,因此可以通过设置请求头来启用此特性[^2]。这使得客户端能够在服务器端数据准备就绪时立即接收部分响应,而不是等待整个响应完成后再获取全部内容。 为了快速搭建带有 Ollama 模型流式响应的应用程序,可以参照如下 Python Flask 示例代码: ```python from flask import Flask, request, Response import requests app = Flask(__name__) @app.route('/chat', methods=['POST']) def chat(): user_input = request.json.get('message') # 发送 POST 请求到 Ollama API 并开启流式读取 response = requests.post( 'http://localhost:11434/v1/chat/completions', json={'prompt': user_input}, stream=True, headers={"Content-Type": "application/json"} ) def generate(): for chunk in response.iter_lines(decode_unicode=True): if chunk: yield f"data:{chunk}\n\n" return Response(generate(), mimetype='text/event-stream') if __name__ == '__main__': app.run(port=8888) ``` 这段代码展示了如何创建一个简单的 Web 应用程序,它接受来自用户的提问并通过 SSE (Server-Sent Events) 协议向浏览器发送实时更新的消息。每当有新的消息块到达时,就会触发一次事件推送至前端显示给用户[^3]。 另外,在 Spring Boot 集成 Ollama 大模型的情况下,可以在 YML 文件中指定基础 URL 及其他参数以便更好地控制和服务于应用需求。例如,设定 `base-url` 为本地主机上的特定端口,并定义所使用的模型及其选项如温度系数等[^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值