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

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;  

http://www.xdnf.cn/news/4663.html

相关文章:

  • C++从入门到实战(十四)初识STL与STL简介
  • [ linux-系统 ] 权限管理
  • Android平台FFmpeg视频解码全流程指南
  • Hadoop MapReduce 图文代码讲解
  • C++ 复习(一)
  • 洛谷 P3811:【模板】模意义下的乘法逆元
  • 监控系统进阶方案:OpenObserve的Docker部署与远程访问配置指南
  • 深入理解 java `isAssignableFrom` 方法
  • Qt 通过控件按钮实现hello world + 命名规范(7)
  • 【QT】: 初识 QWidget 控件 | QWidget 核心属性(API) | qrc 文件
  • 【JavaScript】二十九、垃圾回收 + 闭包 + 变量提升
  • 射频前端模组芯片(PA)三伍微电子GSR2337 兼容替代SKY85337, RTC7646, KCT8247HE
  • 组合优化中常用的数据结构
  • Linux系统(OpenEuler22.03-LTS)部署FastGPT
  • 《构建社交应用用户激励引擎:React Native与Flutter实战解析》
  • 2025年3月电子学会等级考试五级题——4、收费站在哪里
  • 安全月演讲比赛活动讲话稿
  • 【deepseek教学应用】001:deepseek如何撰写教案并自动实现word排版
  • 关于MySQL 数据库故障排查指南
  • 「Mac畅玩AIGC与多模态26」开发篇22 - 多项兴趣格式化建议输出工作流示例
  • debian安装docker
  • 克里金模型+多目标优化+多属性决策!Kriging+NSGAII+熵权TOPSIS!
  • GoWeb开发
  • JWT深度解析:现代Web身份验证的通行证-优雅草卓伊凡
  • vue3的深入组件-组件 v-model
  • jquery+ajax+SpringBoot实现前后端分离技术
  • React Native基础环境配置
  • 自学嵌入式 day 16-c语言-第10章 指针
  • 基础算法 —— 二分算法 【复习总结】
  • Ubuntu Linux系统配置账号无密码sudo