Skip to content

5.6 Add Copilot Chat To UI

In this step, you will review the CopilotChat component of the front-end application and enable it so you can chat with your copilot via the Portal UI. You can create an interactive and responsive chat interface that enhances user engagement by leveraging React's dynamic component-based architecture and its seamless integration with real-time messaging APIs. The copilot chat feature empowers the application to provide real-time conversation capabilities, handle complex state management, and deliver intuitive interactions that make the user experience more engaging and efficient.

Review Copilot Chat UI Component

The Woodgrove Bank Contract Management Portal is a single-page application (SPA) built using React.js, a popular JavaScript framework for interactive user interfaces.

A React component, CopilotChat, has been provided to allow you to easily integrate the copilot capability into the UI of the portal application. This component is implemented in the src/userportal/src/components/CopilotChat.jsx file. Open it now in VS Code and explore the code in sections. You can also expand the section below to see the code inline and review the explanations for each line of code.

Copilot Chat REACT component code.
src/userportal/src/components/CopilotChat.jsx
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
import React, { useState, useEffect, useRef } from 'react';
import ReactMarkdown from 'react-markdown';
import { Row, Col, Button, OverlayTrigger, Tooltip } from 'react-bootstrap';
import ConfirmModal from './ConfirmModal'; 
import api from '../api/Api'; // Adjust the path as necessary
import './CopilotChat.css';

const CopilotChat = () => {
  const [sessionId, setSessionId] = useState(-1);
  const [messages, setMessages] = useState([]);
  const [input, setInput] = useState('');
  const messagesEndRef = useRef(null);
  const [error, setError] = useState('');
  const [isThinking, setIsThinking] = useState(false);

  const [sessions, setSessions] = useState([]);
  const [sessionToDelete, setSessionToDelete] = useState(null);
  const [showDeleteModal, setShowDeleteModal] = useState(false);

  const handleSendMessage = async () => {
    if (input.trim() === '') return;

    const prompt = input;
    setInput('');

    setIsThinking(true);

    // Add the user's message to the local mesage history
    const userMessage = { role: 'user', content: prompt };
    setMessages([...messages, userMessage]);

    setError('');

    try {
      // Get the completion from the API
      const output = await api.completions.chat(sessionId, prompt);

      // make sure request for a different session doesn't update the messages
      if (sessionId === output.session_id) {
        // Add the assistant's response to the messages
        const assistantMessage = { role: 'assistant', content: output.content };
        setMessages([...messages, userMessage, assistantMessage]);
      }

      // only update the messages if the session ID is the same
      // This keeps a processing completion from updating messages after a new session is created
      if (sessionId === -1 || sessionId !== output.session_id) {
        // Update the session ID
        setSessionId(output.session_id);
      }
    } catch (error) {
      console.error('Error sending message:', error);
      setError('Error sending message. Please try again.');
    } finally {
        setIsThinking(false);
    }

  };

  const createNewSession = async () => {
    setSessionId(-1);
    setMessages([]);
    setIsThinking(false);
    setError('');
  };

  const refreshSessionList = async () => {
    try {
      const data = await api.completions.getSessions();
      setSessions(data);
    } catch (error) {
      console.error('Error loading session history:', error);
      setError('Error loading session history. Please try again.');
    }
  }

  const loadSessionHistory = async () => {
    if (!sessionId || sessionId <= 0) {
      setMessages([]);
      return;
    }
    try {
      const data = await api.completions.getHistory(sessionId);
      setMessages(data);
    } catch (error) {
      console.error('Error loading session history:', error);
      setError('Error loading session history. Please try again.');
    }
  }

  useEffect(() => {
    refreshSessionList();
    loadSessionHistory();
  }, [sessionId]);

  useEffect(() => {
    if (messagesEndRef.current) {
      messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
    }
  }, [messages]);

  useEffect(() => {
    refreshSessionList();
  }, []);

  const handleDelete = async () => {
    if (!sessionToDelete) return;

    setError(null);
    try {
      await api.completions.deleteSession(sessionToDelete);

      console.log('Session deleted:', sessionToDelete);
      console.log('Current session:', sessionId);
      if (sessionId === sessionToDelete) {
        setSessionId(-1);
      }
    } catch (err) {
      console.error('Error deleting session:', err);
      setError('Error deleting session. Please try again.');
    }
    setShowDeleteModal(false);
    refreshSessionList();
  };

  return (
    <div className="ai-chat container mt-4">
      <Row>
        <Col style={{ width: '10%', maxWidth: '10em' }}>
          <Row>
            <Button area-label="New Session" alt="New Session" onClick={createNewSession}>
              <i className="fas fa-plus"></i> Chat
            </Button>
          </Row>
          <Row className="mt-3">
            <strong>Chat History</strong>
            {!sessions || sessions.length === 0 && <p>No sessions</p>}
            {sessions && sessions.length > 0 && <ul className="session-list">
              {sessions.map((session, index) => (
                  <li key={index}
                    className={`session ${sessionId === session.id ? 'selected' : ''}`}
                    style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '10px', borderBottom: '1px solid #ccc', cursor: 'pointer' }}
                    onClick={() => setSessionId(session.id)}
                  >
                    <OverlayTrigger
                      placement="top"
                      delay={{ show: 250, hide: 400 }}
                      overlay={<Tooltip id={`tooltip-${index}`}>{session.name.substring(0, 300)}</Tooltip>}
                    >
                      <a alt={session.name}>{session.name}</a>
                    </OverlayTrigger>
                    <div>
                      <OverlayTrigger
                        placement="top"
                        delay={{ show: 250, hide: 400 }}
                        overlay={<Tooltip id={`delete-tooltip-${index}`}>Delete Session</Tooltip>}
                      >
                        <Button className="btn-danger" style={{ marginRight: '10px' }}
                          title="Delete Session"
                          onClick={(e) => { setSessionToDelete(session.id); setShowDeleteModal(true); e.stopPropagation(); }}>
                          <i className="fas fa-trash"></i>
                        </Button>
                      </OverlayTrigger>
                    </div>
                  </li>
                ))}
              </ul>}
          </Row>
        </Col>
        <Col>
          <div className="messages mb-3 border p-3" style={{ minHeight: '20em', maxHeight: '50em', overflowY: 'scroll' }}>
            {messages.map((msg, index) => (
              <div key={index} className={`message ${msg.role} mb-2 d-flex ${msg.role === 'user' ? 'justify-content-end' : 'justify-content-start'}`}>
                {!error && index === messages.length - 1 && <div ref={messagesEndRef} />}
                <div className={`alert ${msg.role === 'user' ? 'alert-primary' : 'alert-secondary'}`} style={{ maxWidth: '90%' }} role="alert">
                  <ReactMarkdown>{msg.content}</ReactMarkdown>
                </div>
              </div>
            ))}
            {error && <div className="alert alert-danger" role="alert">{error}<div ref={messagesEndRef} /></div>}
            {isThinking && <div className="d-flex justify-content-center">
                <div className="spinner-border text-info" role="status">
                  <span className="visually-hidden">Thinking...</span>
                </div>
                <div ref={messagesEndRef} />
              </div>}
          </div>
          <div className="input-container d-flex">
            <textarea className="form-control me-2"
              value={input}
              onChange={(e) => setInput(e.target.value)}
              onKeyDown={(e) => { if (e.key === 'Enter') { handleSendMessage(e); e.preventDefault(); return false; } }}
              placeholder="Type a message..."
            ></textarea>
            <Button onClick={handleSendMessage}>Send</Button>
          </div>
        </Col>
      </Row>

      <ConfirmModal
        show={showDeleteModal}
        handleClose={() => setShowDeleteModal(false)}
        handleConfirm={handleDelete}
        message="Are you sure you want to delete this session?"
      />
    </div>
  );
};

export default CopilotChat;
  1. Import components and libraries (lines 1-6): Required components and libraries are imported.

  2. Define the CopilotChat functional component (line 8). Components in React.js are created using a functional component or class component.

  3. Declare state variables (lines 9-18): State variables are used to maintain the chat session state, including message history.

  4. Provide function for sending messages to the API (lines 20-58): The handleSendMessage function sends messages asynchronously to the Woodgrove Bank API. This function handles the UI's interaction with the backend /completions/chat endpoint, sending the user query and session id to the API.

  5. Provide functions for handling loading chat sessions (lines 60-89): The createNewSession function sets up the state variables to start a new chat session, the refreshSessionList function handles the UI's interaction with the backend /completions endpoint for loading previous chat sessions, and the loadSessionHistory function handles the UI's interaction with the backend /completions endpoint for loading the chat history for the selected chat session.

  6. Handle changes in the Session ID (lines 91-94): The useEffect hook is used to run code in response to changes in the sessionId to refresh the chat session list and load the chat history for the newly selected chat session.

  7. Handle changes in the messages collection (lines 96-100): The useEffect hook is used to run code in response to changes in the messages array.

  8. Handle loading chat session list on page load (lines 102-104): The useEffect hook is used to run code that loads the list of previous chat sessions into the UI on page load.

  9. Provide function for deleting chat session (lines 106-124): The handleDelete function handles the UI's interaction with the backend /completions endpoint for deleting the chosen chat session.

  10. Return the component (lines 126-208): The return statement renders the component's JSX, defining how it is presented in the web browser.

  11. Export the CopilotChat component (line 210): Exports the CopilotChat component as the default export.

Enable the Copilot Chat UI

To enable the copilot chat feature in the Woodgrove Bank Contract Management Portal UI, you will add a reference to the component on the Dashboard page of the UI. The dashboard page is defined in the src/userportal/src/pages/dashboard/dashboard.jsx file. Expand the section below to review the code for the page below.

Dashboard page code
src/userportal/src/pages/dashboard/dashboard.jsx
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import React from 'react';

const Dashboard = () => {
  return (
    <div className="table-responsive">
      <div className="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
        <h1 className="h2">Dashboard</h1>
      </div>

    </div>
  );
};

export default Dashboard;
  1. In the VS Code Explorer, navigate to the src/userportal/src/pages/dashboard folder and open the dashboard.jsx file.

  2. To import the AI chat component, insert the following import statement directly below the import React from 'react'; line at the top of the file.

    Paste the following import statement into dashboard.jsx!

    JavaScript
    1
    import CopilotChat from '../../components/CopilotChat';
    
  3. Insert the following code below the closing tag of <div> containing the Dashboard header (line 9). This will insert the component within the const Dashboard =() => {} functional component block of the dashboard page.

    Paste the following component code into dashboard.jsx!

    JavaScript
    1
    <CopilotChat />
    
  4. The final Dashboard code should look like the following:

    src/userportal/src/pages/dashboard/dashboard.jsx
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    import React from 'react';
    import CopilotChat from '../../components/CopilotChat';
    
    const Dashboard = () => {
      return (
        <div className="table-responsive">
          <div className="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
            <h1 className="h2">Dashboard</h1>
          </div>
          <CopilotChat />
        </div>
      );
    };
    
    export default Dashboard;