当前位置: 首页 > news >正文

React -> AI组件 -> 调用Ollama模型, qwen3:1.7B非常聪明

使用 React 搭建一个现代化的聊天界面,支持与 Ollama 本地部署的大语言模型进行多轮对话。界面清爽、功能完整,支持 Markdown 渲染、代码高亮、<think> 隐藏思考标签、流式渐进反馈、暗黑模式适配等特性。


🧩 核心功能亮点

✅ 模型选择支持
  • 启动时自动请求 http://localhost:11434/api/tags 获取所有本地模型。

  • 允许用户通过下拉框动态切换聊天使用的模型。

✅ 多轮对话支持
  • 聊天上下文由历史消息 messages 组成,发送请求时一并传入。

  • 用户每次发送内容后,bot 的响应将基于历史记录生成。

✅ 实时流式响应 + <think> 处理
  • 使用 ReadableStream 实现逐段渲染。

  • <think>...</think> 区块被识别并自动隐藏,直到关闭 </think> 后再更新 UI。

✅ Markdown 渲染 & 代码高亮
  • 借助 react-markdown + remark-gfm 支持 GitHub 风格 Markdown。

  • 使用 react-syntax-highlighter 实现代码块高亮显示,自动识别语言。

✅ 响应式 UI & 暗黑模式适配
  • 使用 Tailwind CSS 快速构建布局。

  • 检测 HTML dark 类名切换对应代码主题(oneLight / oneDark)。


import React, { useState, useRef, useEffect } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { oneLight, oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';type Message = { text: string; sender: 'user' | 'bot' };type Props = { value: string; onChange: (e: React.ChangeEvent<HTMLInputElement>) => void; onSend: () => void };
const ChatInput: React.FC<Props> = React.memo(({ value, onChange, onSend }) => (<div className="mt-2 flex"><inputclassName="flex-1 px-3 py-2 border rounded-l"value={value}onChange={onChange}onKeyDown={e => e.key === 'Enter' && onSend()}/><button onClick={onSend} className="px-4 bg-neutral-600 text-white rounded-r">发送</button></div>
));const ChatWindow: React.FC = () => {const [models, setModels] = useState<string[]>([]);const [selectedModel, setSelectedModel] = useState<string>('');const [messages, setMessages] = useState<Message[]>([{ text: '你好,我是 Ollama!请选择模型后开始聊天。', sender: 'bot' },]);const [input, setInput] = useState('');const [isThinking, setIsThinking] = useState(false);const messagesEndRef = useRef<HTMLDivElement>(null);const isDark = document.documentElement.classList.contains('dark');const scrollToBottom = () => messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });useEffect(scrollToBottom, [messages, isThinking]);// 获取模型列表useEffect(() => {fetch('http://localhost:11434/api/tags').then(res => res.json()).then(data => {const names = data.models?.map((m: any) => m.name) || [];setModels(names);if (names.length) setSelectedModel(names[0]);}).catch(err => {console.error('获取模型失败:', err);setMessages(prev => [...prev, { text: '无法获取模型列表', sender: 'bot' }]);});}, []);const handleSend = async () => {if (!input.trim() || !selectedModel) return;// 1. 把用户消息加入setMessages(prev => [...prev, { text: input, sender: 'user' }]);setInput('');// 2. 预插入一条 bot 占位,用于后面一次性更新setMessages(prev => [...prev, { text: '', sender: 'bot' }]);// 清洗 <think>…</think> 的工具const cleanThink = (text: string) => text.replace(/<think>[\s\S]*?<\/think>/g, '');let fullText = '';let thinkOpen = false;  // 标记是否在 <think>…</think> 区间try {const response = await fetch('http://localhost:11434/api/chat', {method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify({model: selectedModel,messages: [{ role: 'user', content: input }],}),});const reader = response.body!.getReader();const decoder = new TextDecoder('utf-8');while (true) {const { value, done } = await reader.read();if (done) break;const chunk = decoder.decode(value, { stream: true });const lines = chunk.split('\n').filter(l => l.trim());for (const line of lines) {try {const data = JSON.parse(line);const c = data.message?.content || '';// 检测思考开始if (c.includes('<think>')) {thinkOpen = true;setIsThinking(true);}fullText += c;// 检测思考结束if (c.includes('</think>')) {thinkOpen = false;setIsThinking(false);// 这时才做一次性更新:清洗掉所有 think 内容,并写入 UIconst display = cleanThink(fullText).trim();setMessages(prev => {const copy = [...prev];copy[copy.length - 1] = { text: display, sender: 'bot' };return copy;});}} catch (e) {console.warn('解析流片段失败:', e);}}}// 如果整个流结束后,之前从未触发 </think>(比如模型不输出 think),那也一次性更新if (!thinkOpen) {// 每次都更新显示const display = cleanThink(fullText).trim();setMessages(prev => {const copy = [...prev];copy[copy.length - 1] = { text: display, sender: 'bot' };return copy;});}} catch (err) {console.error('请求出错:', err);setMessages(prev => [...prev,{ text: '请求出错,请检查服务是否开启。', sender: 'bot' },]);setIsThinking(false);}};return (<div className="h-screen flex flex-col p-4 bg-gray-100 dark:bg-gray-900">{/* 模型选择 */}<div className="mb-2"><label className="mr-2 text-sm text-gray-700 dark:text-gray-300">选择模型:</label><selectvalue={selectedModel}onChange={e => setSelectedModel(e.target.value)}className="p-1 text-sm border rounded dark:bg-gray-700 dark:text-white">{models.map(m => (<option key={m} value={m}>{m}</option>))}</select></div>{/* 聊天记录 */}<div className="flex-1 overflow-y-auto p-4 space-y-4 bg-white dark:bg-gray-800 rounded">{/* 聊天记录渲染 */}{messages.map((msg, i) => (<div key={i} className={msg.sender === 'bot' ? '' : 'text-right'}>{msg.sender === 'bot' ? (<div className="prose dark:prose-invert"><ReactMarkdownremarkPlugins={[remarkGfm]}components={{code(props: any) {const { inline, className, children, ...rest } = props;const match = /language-(\w+)/.exec(className || '');if (!inline && match) {return (<SyntaxHighlighterstyle={isDark ? oneDark : oneLight}language={match[1]}PreTag="div"{...rest}>{String(children).replace(/\n$/, '')}</SyntaxHighlighter>);}return (<code className="bg-gray-200 dark:bg-gray-700 px-1 rounded text-sm" {...rest}>{children}</code>);}}}>{msg.text}</ReactMarkdown></div>) : (<div className="inline-block px-3 py-1 bg-neutral-300 dark:bg-neutral-600 rounded text-sm">{msg.text}</div>)}</div>))}{isThinking && <div className="italic text-gray-500">正在思考中…</div>}<div ref={messagesEndRef} /></div>{/* 输入区 */}<ChatInput value={input} onChange={e => setInput(e.target.value)} onSend={handleSend} /></div>);
};export default ChatWindow;  

相关文章:

  • 基础算法系列——树的入门
  • Linux_进程退出与进程等待
  • (14)Element Plus项目综合案例
  • 在Hugging Face网站像Github一样克隆repository到本地的具体步骤
  • bootstrap入门
  • 从代码学习深度学习 - 单发多框检测(SSD)PyTorch版
  • nputop:交互式 Ascend NPU 进程查看器(nvitop昇腾版)
  • 谷云科技iPaaS技术实践:集成平台如何解决库存不准等问题
  • Pycharm安装后打开提示:此应用无法在你的电脑上运行,若要找到合适于你的电脑的版本,请咨询发布者
  • Sass @import rules are deprecated and will be removed in Dart Sass 3.0.0.
  • Docker 核心目录结构
  • Python从入门到高手8.3节-元组的常用操作方法
  • Spark存储级别
  • 系统架构设计-真题2024下半年总结
  • Android不能下载Gradle,解决方法Could not install Gradle distribution from.......
  • 第T11周:优化器对比实验
  • Windows环境下MySQL Installer安装后执行`mysql`和`mysql -v`报错的问题解决方法
  • Centos离线安装mysql、redis、nginx等工具缺乏层层依赖的解决方案
  • BUCK-BOOST调试常见问题总结
  • BUUCTF——杂项渗透之1和0的故事
  • 湖南省职业病防治院通报3岁女童确诊“铊中毒”:去年病例,编辑误写为“近日”
  • 视频丨雄姿英发!中国仪仗队步入莫斯科红场
  • 欧洲史上最严重停电事故敲响警钟:能源转型如何保证电网稳定?
  • 美乌基金协议:美国搞了一套可在资源富集地区复刻的商业模式
  • 江苏省人社厅党组书记、厅长王斌接受审查调查
  • 中信银行:拟出资100亿元全资设立信银金融资产投资有限公司