环境变量设置如下:
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>