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.mzph.cn/news/905414.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vue2/3 中使用 @vue-office/docx 在网页中预览(docx、excel、pdf)文件

1. 安装依赖&#xff1a; #docx文档预览组件npm install vue-office/docx vue-demi0.14.6#excel文档预览组件npm install vue-office/excel vue-demi0.14.6#pdf文档预览组件npm install vue-office/pdf vue-demi0.14.6 vue2.6版本或以下还需要额外安装 vue/composition-api …

【应用密码学】实验五 公钥密码2——ECC

一、实验要求与目的 1.复习CCC基本概念&#xff0c;并根据实验平台提供的资料完成验证性实验。 2.编程练习&#xff1a;以书上例题小模数p为例编程实现ECC的基本运算规则。 二、实验内容与步骤记录&#xff08;只记录关键步骤与结果&#xff0c;可截图&#xff0c;但注意排版…

rust-candle学习笔记9-使用tokenizers加载qwen3分词,使用分词器处理文本

参考&#xff1a;about-pytorch&#xff0c; about-tokenizers 在魔搭社区链接下载qwen3的tokenizer.json文件 添加依赖库&#xff1a; cargo add tokenizers tokenizers库初体验&#xff1a; use tokenizers::tokenizer::{self, Result, Tokenizer};fn main() -> Resu…

【MySQL】存储引擎 - ARCHIVE、BLACKHOLE、MERGE详解

&#x1f4e2;博客主页&#xff1a;https://blog.csdn.net/2301_779549673 &#x1f4e2;博客仓库&#xff1a;https://gitee.com/JohnKingW/linux_test/tree/master/lesson &#x1f4e2;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01; &…

5.Redission

5.1 前文锁问题 基于 setnx 实现的分布式锁存在下面的问题&#xff1a; 重入问题&#xff1a;重入问题是指 获得锁的线程可以再次进入到相同的锁的代码块中&#xff0c;可重入锁的意义在于防止死锁&#xff0c;比如 HashTable 这样的代码中&#xff0c;他的方法都是使用 sync…

C语言主要标准版本的演进与核心区别的对比分析

以下是C语言主要标准版本的演进与核心区别的对比分析 K&R C&#xff08;1978年&#xff09; 定位‌&#xff1a;非标准化的原始版本&#xff0c;由Brian Kernighan和Dennis Ritchie定义 特性‌&#xff1a; 基础语法&#xff1a;函数声明无参数列表&#xff08;如int func…

【C++设计模式之Template Method Pattern】

C设计模式之Template Method Pattern 模式定义核心思想动机(Motivation)结构&#xff08;Structure&#xff09;实现步骤应用场景要点总结 模式定义 模式定义&#xff1a; 定义一个操作中的算法的骨架(稳定)&#xff0c;而将一些步骤延迟(变化)到子类中。Template Method使得子…

【动态导通电阻】p-GaN HEMTs正向和反向导通下的动态导通电阻

2024 年,浙江大学的 Zonglun Xie 等人基于多组双脉冲测试方法,研究了两种不同技术的商用 p-GaN 栅极 HEMTs 在正向和反向导通模式以及硬开关和软开关条件下的动态导通电阻(RON)特性。实验结果表明,对于肖特基型 p-GaN 栅极 HEMTs,反向导通时动态 RON 比正向导通高 3%-5%;…

PDFMathTranslate:科学 PDF 文件翻译及双语对照工具

PDFMathTranslate&#xff1a;科学 PDF 文件翻译及双语对照工具 在科研和学习过程中&#xff0c;我们经常会遇到大量的英文 PDF 文献&#xff0c;翻译这些文献成为了一项繁琐且耗时的工作。PDFMathTranslate 是一款强大的科学 PDF 文件翻译及双语对照工具&#xff0c;它能够保…

Flutter PIP 插件 ---- 为iOS 重构PipController, Demo界面,更好的体验

接上文 Flutter PIP 插件 ---- 新增PipActivity&#xff0c;Android 11以下支持自动进入PIP Mode 项目地址 PIP&#xff0c; pub.dev也已经同步发布 pip 0.0.3&#xff0c;你的加星和点赞&#xff0c;将是我继续改进最大的动力 在之前的界面设计中&#xff0c;还原动画等体验一…

【Ansible】之inventory主机清单

前言 本篇博客主要解释Ansible主机清单的相关配置知识 一、inventory 主机清单 Inventory支持对主机进行分组&#xff0c;每个组内可以定义多个主机&#xff0c;每个主机都可以定义在任何一个或多个主机组内。 如果是名称类似的主机&#xff0c;可以使用列表的方式表示各个主机…

基于几何布朗运动的股价预测模型构建与分析

基于几何布朗运动的股价预测模型构建与分析 摘要 本文建立基于几何布朗运动的股价预测模型&#xff0c;结合极大似然估计与蒙特卡洛模拟&#xff0c;推导股价条件概率密度函数并构建动态预测区间。实证分析显示模型在标普500指数预测中取得89%的覆盖概率&#xff0c;波动率估…

【前端】【JavaScript】【总复习】四万字详解JavaScript知识体系

JavaScript 前端知识体系 &#x1f4cc; 说明&#xff1a;本大纲从基础到高级、从语法到应用、从面试到实战&#xff0c;分层级讲解 JavaScript 的核心内容。 一、JavaScript 基础语法 1.1 基本概念 1.1.1 JavaScript 的发展史与用途 1. 发展简史 1995 年&#xff1a;JavaS…

[Java实战]Spring Boot 3 整合 Apache Shiro(二十一)

[Java实战]Spring Boot 3 整合 Apache Shiro&#xff08;二十一&#xff09; 引言 在复杂的业务系统中&#xff0c;安全控制&#xff08;认证、授权、加密&#xff09;是核心需求。相比于 Spring Security 的重量级设计&#xff0c;Apache Shiro 凭借其简洁的 API 和灵活的扩…

PyTorch API 6 - 编译、fft、fx、函数转换、调试、符号追踪

文章目录 torch.compiler延伸阅读 torch.fft快速傅里叶变换辅助函数 torch.func什么是可组合的函数变换&#xff1f;为什么需要可组合的函数变换&#xff1f;延伸阅读 torch.futurestorch.fx概述编写转换函数图结构快速入门图操作直接操作计算图使用 replace_pattern() 进行子图…

可观测性方案怎么选?SelectDB vs Elasticsearch vs ClickHouse

可观测性&#xff08;Observability&#xff09;是指通过系统的外部输出数据&#xff0c;推断其内部状态的能力。可观测性平台通过采集、存储、可视化分析三大可观测性数据&#xff1a;日志&#xff08;Logging&#xff09;、链路追踪&#xff08;Tracing&#xff09;和指标&am…

机器人厨师上岗!AI在餐饮界掀起新风潮!

想要了解人工智能在其他各个领域的应用&#xff0c;可以查看下面一篇文章 《AI在各领域的应用》 餐饮业是与我们日常生活息息相关的行业&#xff0c;而人工智能&#xff08;AI&#xff09;正在迅速改变这个传统行业的面貌。从智能点餐到食材管理&#xff0c;再到个性化推荐&a…

Linux动态库静态库总结

静态库生成 g -c mylib.cpp -o mylib.o ar rcs libmylib.a mylib.o 动态库生成 g -fPIC -shared mylib.cpp -o libmylib.so -fPIC&#xff1a;生成位置无关代码&#xff08;Position-Independent Code&#xff09;&#xff0c;对动态库必需。 库文件使用&#xff1a; 静态库&…

通过user-agent来源判断阻止爬虫访问网站,并防止生成[ error ] NULL日志

一、TP5.0通过行为&#xff08;Behavior&#xff09;拦截爬虫并避免生成 [ error ] NULL 错误日志 1. 创建行为类&#xff08;拦截爬虫&#xff09; 在 application/common/behavior 目录下新建BlockBot.php &#xff0c;用于识别并拦截爬虫请求&#xff1a; <?php name…

OpenHarmony平台驱动开发(十五),SDIO

OpenHarmony平台驱动开发&#xff08;十五&#xff09; SDIO 概述 功能简介 SDIO&#xff08;Secure Digital Input and Output&#xff09;由SD卡发展而来&#xff0c;与SD卡统称为MMC&#xff08;MultiMediaCard&#xff09;&#xff0c;二者使用相同的通信协议。SDIO接口…