<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      React(二):構建一個簡單的聊天助手學到的React知識

      前言

      先來看下效果。

      非流式(基本不會用):

      流式:

      其實創建React項目一般都會直接使用組件庫,比如Ant-Design,就比如構建聊天助手,其實使用Ant-Design-X就比較好,但是畢竟Ant-Design-X太新了,AI估計寫的不太好,需要自己看文檔,后面可以考慮用下Ant-Design-X。剛開始學習可以先不用組件,直接把界面交給AI,樣式也直接讓AI來寫。等到對React有一些基礎的了解了,就可以自己上手看組件庫的文檔,去使用組件庫了。畢竟AI寫的界面維護起來挺麻煩的,一堆.css文件,隨著界面的增加,.css樣式也越多,后面要改哪塊自己也不太清楚,當然寫Demo無所謂了,直接交給AI即可。

      總結一下在構建這個簡單的聊天助手中學到的React知識,包含React本身的概念與相關生態。

      項目結構

      WPF中如果有一個服務邏輯很多個頁面都可以共用,我們一般會新建一個Service文件夾,把這個共用服務放到這個位置,就比如說向LLM發送一個API請求,這個多個頁面都要用到,如果只是寫在某一個頁面的按鈕點擊事件中,那么就要寫很多次,而且頁面包含了太多邏輯,不好維護。

      問AI之后,AI給出的方案是增加types、services與hooks,如下所示:

      types與services大概知道是什么東西,types就是相當于WPF中我們寫的Model,定義了一些數據結構:

      作用:統一定義前端與服務層之間傳遞的結構化數據的類型,保證調用處與實現處在編譯期即可發現不匹配。

      Services中就是封裝了API請求,如下所示:

      作用:對外部系統(HTTP、SSE、WebSocket 等)進行通信封裝;聚焦網絡協議、錯誤處理、數據解析與重試等細節,不參與 UI 狀態管理。

      目前就幾個方法,就使用了靜態方法即可。在WPF中一般是將服務類注入到容器中,但目前沒有接觸到React中有這樣操作,而是采用了一個自定義hook。

      現在來看看這個自定義hook:

      作用:圍繞 React 狀態構建可復用的“用例邏輯”,把服務結果轉化為組件可直接使用的狀態與方法(messages、輸入框、loading、發送/清空等)。

      自定義hook是React中一個很重要的概念,現在來我們來了解一下。

      自定義 Hook 是把“可復用的狀態與副作用邏輯”封裝成以 use 開頭的函數,供多個組件共享,不直接渲染 UI。

      使用自定義Hook有什么好處?

      1、復用與一致性

      多頁面/多個組件可以共用同一份聊天邏輯,無需重復粘貼網絡請求與狀態管理代碼。
      任一處修復或優化(例如流式解析、錯誤提示格式)會自動惠及所有使用該 Hook 的組件。

      2、狀態編排集中、UI 簡化

      就是把一些原本寫在UI中的邏輯提取出來,避免了在UI中寫太多邏輯。

      在我這個Demo中,整體結構是這樣的:

      現在大概了解了一下自定義Hook,讓我們看看在組件中是如何使用的:

      首先導入這個自定義Hook,會發現這個Hook已經自帶了一些屬性與方法,然后在這個UI組件中直接使用這些屬性與方法即可,現在我們的這個組件的代碼量比起之前已經大大減少了。

      React Hook

      現在遇到了兩個ReactHook,分別是useState與useCallback。

      useState用法示例:

        const [inputText, setInputText] = useState('');
      

      剛開始學習推薦先看下文檔,地址:https://react.dev/reference/react/useState

      useState 是 React 的一個 Hook,它允許你在組件中添加一個狀態變量。

      useCallback用法示例:

        const clearMessages = useCallback(() => {
          setMessages(initialMessages);
        }, []);
      

      文檔地址:https://react.dev/reference/react/useCallback

      useCallback 是一個 React Hook,它允許你在重新渲染之間緩存函數定義。

      React 的 useCallback() 用來“記憶”回調函數的引用,只有依賴列表中的值變化時才重新創建函數。

      它的價值在于:傳給使用 React.memo 或在 useEffect 里使用的子組件/第三方庫時,避免無意義的重新執行。當某回調本身被其他鉤子/效果的依賴引用時,防止依賴變化導致的循環或多余重跑。

      看了一下解釋感覺還是有點迷,我們只要搞清楚為什么我們在這個自定義hook中要使用useCallback就行了。

      穩定引用,減少下游重渲染:這些回調會被 hook 返回并作為 props 傳給頁面或子組件。如果不使用 useCallback(),每次父組件渲染都會得到新的函數引用,導致使用 React.memo 的子組件或依賴回調引用的庫產生不必要的重渲染或重復訂閱。

      控制依賴觸發時機,避免無意義的副作用重跑:消費者很可能在 useEffect 或 useMemo 的依賴數組里引用這些回調。通過 useCallback() 明確依賴變更時機,保證只有在真正相關的狀態變動時才更新回調引用,從而避免“每次渲染都觸發 effect”的問題。

      維持事件處理器和鏈式依賴的穩定性:handleKeyPress() 依賴并調用 sendMessage()。如果不使用 useCallback,sendMessage 每次渲染都會變,進而導致 handleKeyPress 每次也變,繼續向下游(輸入框/按鈕等)傳播不必要的引用變化。

      react生態:react-markdown

      學習使用React除了學習React本身的概念與設計思想外,很重要的一點就是多接觸一些react生態。

      在構建一個簡單的聊天助手很重要的一點就是渲染md格式文本,因為LLM比較喜歡返回md格式內容,如果不渲染直接顯示,會不好看,很多 ** ## 這類符號。在React中渲染md格式內容,說實話比在WPF中簡單多了,WPF中渲染md內容,目前還沒找到一個比較好用的解決方案。

      在React中丟給AI就能寫很多東西,AI寫了一個MarkdownRenderer組件:

      import React from 'react';
      import ReactMarkdown from 'react-markdown';
      import remarkGfm from 'remark-gfm';
      import remarkBreaks from 'remark-breaks';
      import './MarkdownRenderer.css';
      
      interface MarkdownRendererProps {
        content: string;
        isStreaming?: boolean;
      }
      
      const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
        content,
        isStreaming = false
      }) => {
        return (
          <div className="markdown-content">
            <ReactMarkdown
              remarkPlugins={[remarkGfm, remarkBreaks]}
              components={{
                h1: ({ children }) => <h1 className="markdown-h1">{children as React.ReactNode}</h1>,
                h2: ({ children }) => <h2 className="markdown-h2">{children as React.ReactNode}</h2>,
                h3: ({ children }) => <h3 className="markdown-h3">{children as React.ReactNode}</h3>,
                h4: ({ children }) => <h4 className="markdown-h4">{children as React.ReactNode}</h4>,
                h5: ({ children }) => <h5 className="markdown-h5">{children as React.ReactNode}</h5>,
                h6: ({ children }) => <h6 className="markdown-h6">{children as React.ReactNode}</h6>,
                p: ({ children }) => <p className="markdown-p">{children as React.ReactNode}</p>,
                ul: ({ children }) => <ul className="markdown-ul">{children as React.ReactNode}</ul>,
                ol: ({ children }) => <ol className="markdown-ol">{children as React.ReactNode}</ol>,
                li: ({ children }) => <li className="markdown-li">{children as React.ReactNode}</li>,
                blockquote: ({ children }) => (
                  <blockquote className="markdown-blockquote">{children as React.ReactNode}</blockquote>
                ),
                code: ({ className, children, ...props }) => {
                  const match = /language-(\w+)/.exec(className || '');
                  const isInline = (props as any).inline ?? !className;
                  if (isInline) {
                    return (
                      <code className="markdown-inline-code" {...props}>
                        {children as React.ReactNode}
                      </code>
                    );
                  }
                  const langClass = match ? className || '' : '';
                  return (
                    <code className={`markdown-code-block ${langClass}`} {...props}>
                      {children as React.ReactNode}
                    </code>
                  );
                },
                pre: ({ children }) => <pre className="markdown-pre">{children as React.ReactNode}</pre>,
                a: ({ href, children }) => (
                  <a href={href} className="markdown-link" target="_blank" rel="noopener noreferrer">
                    {children as React.ReactNode}
                  </a>
                ),
                strong: ({ children }) => <strong className="markdown-strong">{children as React.ReactNode}</strong>,
                em: ({ children }) => <em className="markdown-em">{children as React.ReactNode}</em>,
                hr: () => <hr className="markdown-hr" />,
                table: ({ children }) => <table className="markdown-table">{children as React.ReactNode}</table>,
                thead: ({ children }) => <thead className="markdown-thead">{children as React.ReactNode}</thead>,
                tbody: ({ children }) => <tbody className="markdown-tbody">{children as React.ReactNode}</tbody>,
                tr: ({ children }) => <tr className="markdown-tr">{children as React.ReactNode}</tr>,
                th: ({ children }) => <th className="markdown-th">{children as React.ReactNode}</th>,
                td: ({ children }) => <td className="markdown-td">{children as React.ReactNode}</td>,
              }}
            >
              {content}
            </ReactMarkdown>
            {isStreaming && (
              <span className="streaming-cursor">|</span>
            )}
          </div>
        );
      };
      
      export default MarkdownRenderer;
      

      了解React的生態,我們可以看看AI使用了什么庫。

      react-markdown介紹

      這個包是一個 React 組件,可以接收一個 Markdown 字符串,并將其安全地渲染為 React 元素。您可以傳遞插件來改變 Markdown 的轉換方式,還可以傳遞組件來替代普通的 HTML 元素。

      GitHub地址:https://github.com/remarkjs/react-markdown

      remark-gfm介紹

      remark 插件支持 GFM(自動鏈接字面量、腳注、刪除線、表格、任務列表)

      GitHub地址:https://github.com/remarkjs/remark-gfm

      remark-breaks介紹

      remark 插件支持在不使用空格或轉義字符的情況下實現硬換行(將回車轉換為
      標簽)。

      GitHub地址:https://github.com/remarkjs/remark-breaks

      主要是第一個,很多時候用第一個就夠了,第二個第三個都只是插件,增加一些功能。

      看下在聊天頁面中如何使用:

      導入這個組件直接使用即可。

      SSE

      我們之前可能接觸到的很多請求模式可能都是發送一個請求收到一個響應這樣。

      就比如非流式響應,你向AI提問一個問題,AI全部返回一下子返回給你。但是在聊天應用中這種模式,用戶或許很難以忍受,因為這種等待的時間要長一點,而且是一下子顯示的。

      所以需要流式響應,而后端向前端流式響應,就需要用到SSE了。

      Server-Sent Events(簡稱 SSE,服務器發送事件)是一種讓服務器主動向客戶端(通常是瀏覽器)推送數據的技術。它基于 HTTP 協議,允許服務器持續發送更新,而客戶端只需建立一次連接,便可接收不斷傳來的消息。

      與傳統的“客戶端請求 → 服務器響應”模式不同,SSE 實現了服務器到客戶端的單向實時通信。

      首先需要先有一個流式返回的接口,創建一個Web API項目,一個流式接口可以這樣寫:

        [HttpGet("ai-response-streaming")]
        public async Task<IActionResult> GetAIResponseStreaming([FromQuery] string prompt)
        {
            try
            {
                if (string.IsNullOrWhiteSpace(prompt))
                {
                    return BadRequest("Prompt cannot be empty.");
                }
      
                _logger.LogInformation("Received streaming AI request with prompt: {Prompt}", prompt);
      
                // 更規范地設置 SSE 響應頭
                Response.ContentType = "text/event-stream";
                Response.Headers["Cache-Control"] = "no-cache";
                Response.Headers["Connection"] = "keep-alive";
      
                await foreach (var chunk in _agentFrameworkService.GetAIResponseStreaming(prompt))
                {
                    if (string.IsNullOrEmpty(chunk)) continue;
      
                    // 規范化換行,并將多行內容按多條 data: 發送,確保前端正確還原 Markdown 的行語義
                    var normalized = chunk.Replace("\r\n", "\n").Replace("\r", "\n");
                    var lines = normalized.Split('\n');
      
                    foreach (var line in lines)
                    {
                        await Response.WriteAsync($"data: {line}\n");
                    }
      
                    // 空行表示一個 SSE 事件結束(等價于原先的 "\n\n")
                    await Response.WriteAsync("\n");
                    await Response.Body.FlushAsync();
                }
      
                await Response.WriteAsync("data: [DONE]\n\n");
                await Response.Body.FlushAsync();
      
                return new EmptyResult();
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error occurred while getting streaming AI response");
                return StatusCode(500, "An error occurred while processing your request.");
            }
        }
      

      在調用的這個服務中返回IAsyncEnumerable<string>內容即可:

       public async IAsyncEnumerable<string> GetAIResponseStreaming(string prompt)
       {
           ApiKeyCredential apiKeyCredential = new ApiKeyCredential(apiKey);
      
           OpenAIClientOptions openAIClientOptions = new OpenAIClientOptions();
           openAIClientOptions.Endpoint = new Uri(endpoint);
      
           AIAgent agent = new OpenAIClient(apiKeyCredential, openAIClientOptions)
               .GetChatClient(model)
               .CreateAIAgent("你是一個有用的助手。");
      
           await foreach (var update in agent.RunStreamingAsync(prompt))
           {
               var chunk = update?.ToString() ?? string.Empty;
      
               // 確保每個分片以換行結束,避免 Markdown 結構(如 #、```csharp)被分片打斷后丟失必要的換行
               if (!chunk.EndsWith("\n"))
               {
                   chunk += "\n";
               }
      
               yield return chunk;
           }
       }
      

      寫好這個接口之后,可以使用自帶的Swagger或者API Fox等工具測試一下流式傳輸效果:

      沒問題之后,后端就先不管了,去看看React中是如何處理的。

      在React中使用fetch來發起這個流式請求:

      總結

      通過構建一個簡單的聊天助手目前我們學習了:

      1、React項目的簡單架構types、services與hooks。

      2、學習了兩個React Hook,分別是useState與useCallback。

      3、學習了在React中渲染md格式內容可以使用react-markdown。

      4、學習了使用SSE實現流式響應。

      posted @ 2025-11-04 14:12  mingupupup  閱讀(106)  評論(2)    收藏  舉報
      主站蜘蛛池模板: 女人香蕉久久毛毛片精品| 国精偷拍一区二区三区| 国产中年熟女高潮大集合| 少妇被粗大的猛烈进出动视频| 成av人电影在线观看| 亚洲自拍偷拍福利小视频| 99久久国产综合精品女同| 阿拉善左旗| 福利一区二区不卡国产| 国产高清精品一区二区三区| 亚洲成a人无码av波多野| 国产成人无码免费视频在线| 2020国产欧洲精品网站| 2019国产精品青青草原| 国产成人精品无码片区在线观看| 国产精品 无码专区| 精品亚洲男人一区二区三区| 久久综合亚洲鲁鲁九月天| 国产一区二区三区精品综合| 亚洲另类激情专区小说图片| 东方四虎av在线观看| 国产做无码视频在线观看浪潮| 福利一区二区不卡国产| 91福利国产午夜亚洲精品| 四虎国产精品永久免费网址| 一区二区三区精品偷拍| 国产婷婷精品av在线| 亚洲A综合一区二区三区| 达孜县| 一区二区三区鲁丝不卡| 亚洲欧美日韩成人综合一区| 左权县| 精品人妻一区二区| 国产精品亚洲av三区色| 国产午夜精品福利免费看| 国产精品一区二区中文| 中文文字幕文字幕亚洲色| 成人aⅴ综合视频国产| 国内精品视频区在线2021| 亚洲丰满熟女一区二区蜜桃| 亚洲鸥美日韩精品久久|