Skip to content

Commit

Permalink
Merge pull request #6 from SkywardAI/wllama
Browse files Browse the repository at this point in the history
Implement chat
  • Loading branch information
cbh778899 authored Sep 10, 2024
2 parents 83287aa + a80c0ad commit 8e83dad
Show file tree
Hide file tree
Showing 19 changed files with 699 additions and 82 deletions.
4 changes: 2 additions & 2 deletions src/components/App.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState } from "react";
import { useState } from "react";
import { createBrowserRouter, Navigate, RouterProvider, } from "react-router-dom";
import Sidebar from "./Sidebar";
import Sidebar from "./sidebar";
import Chat from "./chat";
import Settings from "./Settings";

Expand Down
2 changes: 0 additions & 2 deletions src/components/Settings.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import React from "react";

export default function Settings() {
return (
<div>
Expand Down
34 changes: 0 additions & 34 deletions src/components/Sidebar.jsx

This file was deleted.

88 changes: 68 additions & 20 deletions src/components/chat/Conversation.jsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,86 @@
import React, { useEffect, useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";
import ConversationBubble from "./ConversationBubble";
import { Send } from 'react-bootstrap-icons';

async function getConversationByUid(uid) {
return [
{ role: "user", content: "Hello!" },
{ role: "assistant", content: "Hi, how can I help you today?" }
]
}
import { Send, StopCircleFill } from 'react-bootstrap-icons';
import useIDB from "../../utils/idb";
import { abortCompletion, chatCompletions, isModelLoaded, loadModel } from '../../utils/worker'

export default function Conversation({ uid }) {

const [conversation, setConversation] = useState([]);
const [message, setMessage] = useState('');
const [pending_message, setPendingMessage] = useState('');
const [hide_pending, setHidePending] = useState(true);
const idb = useIDB();

const bubblesRef = useRef();

async function getConversationByUid() {
setConversation(
await idb.getAll(
'messages',
{
where: [{'history-uid': uid}],
select: ['role', 'content']
}
)
);
}

function messageOnChange(evt) {
setMessage(evt.target.value);
}

function sendMessage(evt) {
async function sendMessage(evt) {
evt.preventDefault();
if(!message) return;
setConversation([...conversation, {role: 'user', content: message}])
if(!message || !hide_pending) return;

idb.insert('messages', {
'history-uid': uid,
role: 'user',
content: message,
createdAt: Date.now()
})
const user_msg = {role: 'user', content: message}
setConversation([...conversation, user_msg])
setMessage('');
setHidePending(false);

if(!isModelLoaded()) {
await loadModel();
}
await chatCompletions([user_msg],
(text, isFinished) => {
if(!isFinished) {
setPendingMessage(text);
} else {
setPendingMessage('');
setConversation([
...conversation, user_msg,
{ role: 'assistant', content: text }
])
idb.insert('messages', {
'history-uid': uid,
role: 'assistant',
content: text,
createdAt: Date.now()
})
setHidePending(true);
}
}
)
}

useEffect(()=>{
if(uid) {
(async function() {
setConversation(await getConversationByUid(uid));
})();
}
uid && getConversationByUid();
// eslint-disable-next-line
}, [uid]);

useEffect(()=>{
bubblesRef.current && bubblesRef.current.scrollTo({
behavior: "smooth",
top: bubblesRef.current.scrollHeight
})
}, [conversation])
}, [conversation, pending_message])

return (
<div className="conversation-main">
Expand All @@ -56,12 +96,20 @@ export default function Conversation({ uid }) {
/>
)
}) }
<ConversationBubble
role={'assistant'} content={pending_message}
hidden={hide_pending}
/>
</div>
<form className="send-message-form" onSubmit={sendMessage}>
<input type="text" value={message} onChange={messageOnChange}/>
<div className="send-message-button-container">
<Send className="button-icon" />
<input type='submit' className="clickable"/>
{
hide_pending ?
<Send className="button-icon" /> :
<StopCircleFill className="button-icon stop clickable" onClick={abortCompletion} />
}
<input type='submit' className={`clickable${!hide_pending?" disabled":''}`}/>
</div>
</form>
</> :
Expand Down
17 changes: 13 additions & 4 deletions src/components/chat/ConversationBubble.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import React from "react";
import { CircleFill } from "react-bootstrap-icons"
import Markdown from "react-markdown"

export default function ConversationBubble({role, content}) {
export default function ConversationBubble({role, content, hidden}) {
return (
<div className={`bubble ${role}`}>
{ content }
<div className={`bubble ${role}${hidden?' hidden':""}`}>
{
content ?
<Markdown>{ content }</Markdown> :
<>
<CircleFill className="dot-animation" />
<CircleFill className="dot-animation" />
<CircleFill className="dot-animation" />
</>
}
</div>
)
}
2 changes: 0 additions & 2 deletions src/components/chat/Ticket.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import React from "react";

export default function Ticket({ title, uid, selectChat, is_selected }) {
return (
<div
Expand Down
45 changes: 39 additions & 6 deletions src/components/chat/Tickets.jsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,49 @@
import React, { useState } from "react";
import { useEffect, useState } from "react";
import Ticket from "./Ticket";
import useIDB from "../../utils/idb";
import { genRandomID } from "../../utils/tools";

export default function Tickets({selectChat, current_chat}) {

const [tickets, setTickets] = useState([
{title: "Hello!", uid: Math.random().toString(32).slice(2)},
{title: "Hello!", uid: Math.random().toString(32).slice(2)},
{title: "Hello!", uid: Math.random().toString(32).slice(2)}
]);
const [tickets, setTickets] = useState([]);
const idb = useIDB();

async function syncHistory() {
setTickets(await idb.getAll('chat-history'))
}

async function startNewConversation() {
const timestamp = Date.now();
const conv_id = await idb.insert("chat-history",
{
title: 'New Conversation',
createdAt: timestamp,
updatedAt: timestamp,
uid: genRandomID()
}
)
const new_conv_info = await idb.getById('chat-history', conv_id);
new_conv_info &&
setTickets([
...tickets,
new_conv_info
])
selectChat(new_conv_info.uid)
}

useEffect(()=>{
syncHistory()
// eslint-disable-next-line
}, [])

return (
<div className="tickets">
<div
className="new-conversation clickable"
onClick={startNewConversation}
>
<div>Start New Chat</div>
</div>
{ tickets.map(elem => {
const { title, uid } = elem;
return (
Expand Down
2 changes: 1 addition & 1 deletion src/components/chat/index.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from "react";
import { useState } from "react";
import Tickets from "./Tickets";
import Conversation from "./Conversation";

Expand Down
10 changes: 10 additions & 0 deletions src/components/sidebar/SidebarIcons.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Link } from "react-router-dom"

export default function SidebarIcon({to, children, pathname}) {

return (
<Link to={to} className={`item${pathname===to?' selected':''}`}>
{ children }
</Link>
)
}
23 changes: 23 additions & 0 deletions src/components/sidebar/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Outlet, useLocation } from "react-router-dom";
import { Chat, Gear } from "react-bootstrap-icons";
import SidebarIcon from "./SidebarIcons";

export default function Sidebar() {
const { pathname } = useLocation();

return (
<div className="main">
<div className="app-sidebar">
<div className="section">
<SidebarIcon to={'/chat'} pathname={pathname}><Chat/></SidebarIcon>
</div>
<div className="section bottom">
<SidebarIcon to={'/settings'} pathname={pathname}><Gear/></SidebarIcon>
</div>
</div>
<div className="window">
<Outlet />
</div>
</div>
)
}
58 changes: 57 additions & 1 deletion src/styles/chat.css
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@
border-right: 1px solid lightgray;
}

.chat > .tickets > .new-conversation {
padding: 10px;
background-color: var(--change-bg-color);
color: rgb(50, 50, 50);
text-align: center;
transition-duration: .3s;
font-size: 15px;
font-weight: bold;
}
.chat > .tickets > .new-conversation:hover {
background-color: rgba(0, 0, 0, .1);
}

.chat > .tickets > .ticket {
--ticket-bg-color: var(--section-bg-color);
background-color: var(--ticket-bg-color);
Expand Down Expand Up @@ -134,6 +147,45 @@
background-color: var(--normal-bg-color);
}

.chat > .conversation-main > .bubbles > .bubble > p {
margin: 0;
}
.chat > .conversation-main > .bubbles > .bubble:has(pre) {
width: 100%;
}
.chat > .conversation-main > .bubbles > .bubble pre {
width: 100%;
background-color: rgb(80, 80, 80);
color: white;
padding: 10px;
overflow: auto;
}
.chat > .conversation-main > .bubbles > .bubble :not(pre) code {
padding: 0px 5px;
background-color: rgb(227, 227, 227);
border-radius: 5px;
}

@keyframes dotAnimation {
0% { color: rgb(90, 90, 90); }
50% { color: rgb(150, 150, 150); }
100% { color: rgb(210, 210, 210); }
}

.chat > .conversation-main > .bubbles > .bubble > .dot-animation {
--size: 13px;
width: var(--size);
height: var(--size);
margin-right: 4px;
animation: dotAnimation 1.2s infinite linear;
}
.chat > .conversation-main > .bubbles > .bubble > .dot-animation:nth-child(2) {
animation-delay: .4s;
}
.chat > .conversation-main > .bubbles > .bubble > .dot-animation:nth-child(3) {
animation-delay: .8s;
}

.chat > .conversation-main > .send-message-form >
input[type="text"] {
width: calc(100% - 20px);
Expand Down Expand Up @@ -163,7 +215,7 @@ input[type="text"]:focus {
}

.chat > .conversation-main > .send-message-form >
.send-message-button-container:hover > .button-icon {
.send-message-button-container:hover > .button-icon:not(.stop) {
transform: rotate(45deg);
}

Expand All @@ -177,6 +229,10 @@ input[type="text"]:focus {
z-index: 2;
opacity: 0;
}
.chat > .conversation-main > .send-message-form >
.send-message-button-container > input[type="submit"].disabled {
pointer-events: none;
}

.chat > .conversation-main > .send-message-form >
.send-message-button-container > .button-icon {
Expand Down
Loading

0 comments on commit 8e83dad

Please sign in to comment.