Skip to content

Commit 8e83dad

Browse files
authored
Merge pull request #6 from SkywardAI/wllama
Implement chat
2 parents 83287aa + a80c0ad commit 8e83dad

19 files changed

+699
-82
lines changed

src/components/App.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import React, { useState } from "react";
1+
import { useState } from "react";
22
import { createBrowserRouter, Navigate, RouterProvider, } from "react-router-dom";
3-
import Sidebar from "./Sidebar";
3+
import Sidebar from "./sidebar";
44
import Chat from "./chat";
55
import Settings from "./Settings";
66

src/components/Settings.jsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import React from "react";
2-
31
export default function Settings() {
42
return (
53
<div>

src/components/Sidebar.jsx

Lines changed: 0 additions & 34 deletions
This file was deleted.

src/components/chat/Conversation.jsx

Lines changed: 68 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,86 @@
1-
import React, { useEffect, useRef, useState } from "react";
1+
import { useEffect, useRef, useState } from "react";
22
import ConversationBubble from "./ConversationBubble";
3-
import { Send } from 'react-bootstrap-icons';
4-
5-
async function getConversationByUid(uid) {
6-
return [
7-
{ role: "user", content: "Hello!" },
8-
{ role: "assistant", content: "Hi, how can I help you today?" }
9-
]
10-
}
3+
import { Send, StopCircleFill } from 'react-bootstrap-icons';
4+
import useIDB from "../../utils/idb";
5+
import { abortCompletion, chatCompletions, isModelLoaded, loadModel } from '../../utils/worker'
116

127
export default function Conversation({ uid }) {
138

149
const [conversation, setConversation] = useState([]);
1510
const [message, setMessage] = useState('');
11+
const [pending_message, setPendingMessage] = useState('');
12+
const [hide_pending, setHidePending] = useState(true);
13+
const idb = useIDB();
1614

1715
const bubblesRef = useRef();
1816

17+
async function getConversationByUid() {
18+
setConversation(
19+
await idb.getAll(
20+
'messages',
21+
{
22+
where: [{'history-uid': uid}],
23+
select: ['role', 'content']
24+
}
25+
)
26+
);
27+
}
28+
1929
function messageOnChange(evt) {
2030
setMessage(evt.target.value);
2131
}
2232

23-
function sendMessage(evt) {
33+
async function sendMessage(evt) {
2434
evt.preventDefault();
25-
if(!message) return;
26-
setConversation([...conversation, {role: 'user', content: message}])
35+
if(!message || !hide_pending) return;
36+
37+
idb.insert('messages', {
38+
'history-uid': uid,
39+
role: 'user',
40+
content: message,
41+
createdAt: Date.now()
42+
})
43+
const user_msg = {role: 'user', content: message}
44+
setConversation([...conversation, user_msg])
2745
setMessage('');
46+
setHidePending(false);
47+
48+
if(!isModelLoaded()) {
49+
await loadModel();
50+
}
51+
await chatCompletions([user_msg],
52+
(text, isFinished) => {
53+
if(!isFinished) {
54+
setPendingMessage(text);
55+
} else {
56+
setPendingMessage('');
57+
setConversation([
58+
...conversation, user_msg,
59+
{ role: 'assistant', content: text }
60+
])
61+
idb.insert('messages', {
62+
'history-uid': uid,
63+
role: 'assistant',
64+
content: text,
65+
createdAt: Date.now()
66+
})
67+
setHidePending(true);
68+
}
69+
}
70+
)
2871
}
2972

3073
useEffect(()=>{
31-
if(uid) {
32-
(async function() {
33-
setConversation(await getConversationByUid(uid));
34-
})();
35-
}
74+
uid && getConversationByUid();
75+
// eslint-disable-next-line
3676
}, [uid]);
3777

3878
useEffect(()=>{
3979
bubblesRef.current && bubblesRef.current.scrollTo({
4080
behavior: "smooth",
4181
top: bubblesRef.current.scrollHeight
4282
})
43-
}, [conversation])
83+
}, [conversation, pending_message])
4484

4585
return (
4686
<div className="conversation-main">
@@ -56,12 +96,20 @@ export default function Conversation({ uid }) {
5696
/>
5797
)
5898
}) }
99+
<ConversationBubble
100+
role={'assistant'} content={pending_message}
101+
hidden={hide_pending}
102+
/>
59103
</div>
60104
<form className="send-message-form" onSubmit={sendMessage}>
61105
<input type="text" value={message} onChange={messageOnChange}/>
62106
<div className="send-message-button-container">
63-
<Send className="button-icon" />
64-
<input type='submit' className="clickable"/>
107+
{
108+
hide_pending ?
109+
<Send className="button-icon" /> :
110+
<StopCircleFill className="button-icon stop clickable" onClick={abortCompletion} />
111+
}
112+
<input type='submit' className={`clickable${!hide_pending?" disabled":''}`}/>
65113
</div>
66114
</form>
67115
</> :
Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
1-
import React from "react";
1+
import { CircleFill } from "react-bootstrap-icons"
2+
import Markdown from "react-markdown"
23

3-
export default function ConversationBubble({role, content}) {
4+
export default function ConversationBubble({role, content, hidden}) {
45
return (
5-
<div className={`bubble ${role}`}>
6-
{ content }
6+
<div className={`bubble ${role}${hidden?' hidden':""}`}>
7+
{
8+
content ?
9+
<Markdown>{ content }</Markdown> :
10+
<>
11+
<CircleFill className="dot-animation" />
12+
<CircleFill className="dot-animation" />
13+
<CircleFill className="dot-animation" />
14+
</>
15+
}
716
</div>
817
)
918
}

src/components/chat/Ticket.jsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import React from "react";
2-
31
export default function Ticket({ title, uid, selectChat, is_selected }) {
42
return (
53
<div

src/components/chat/Tickets.jsx

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,49 @@
1-
import React, { useState } from "react";
1+
import { useEffect, useState } from "react";
22
import Ticket from "./Ticket";
3+
import useIDB from "../../utils/idb";
4+
import { genRandomID } from "../../utils/tools";
35

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

6-
const [tickets, setTickets] = useState([
7-
{title: "Hello!", uid: Math.random().toString(32).slice(2)},
8-
{title: "Hello!", uid: Math.random().toString(32).slice(2)},
9-
{title: "Hello!", uid: Math.random().toString(32).slice(2)}
10-
]);
8+
const [tickets, setTickets] = useState([]);
9+
const idb = useIDB();
10+
11+
async function syncHistory() {
12+
setTickets(await idb.getAll('chat-history'))
13+
}
14+
15+
async function startNewConversation() {
16+
const timestamp = Date.now();
17+
const conv_id = await idb.insert("chat-history",
18+
{
19+
title: 'New Conversation',
20+
createdAt: timestamp,
21+
updatedAt: timestamp,
22+
uid: genRandomID()
23+
}
24+
)
25+
const new_conv_info = await idb.getById('chat-history', conv_id);
26+
new_conv_info &&
27+
setTickets([
28+
...tickets,
29+
new_conv_info
30+
])
31+
selectChat(new_conv_info.uid)
32+
}
33+
34+
useEffect(()=>{
35+
syncHistory()
36+
// eslint-disable-next-line
37+
}, [])
1138

1239
return (
1340
<div className="tickets">
41+
<div
42+
className="new-conversation clickable"
43+
onClick={startNewConversation}
44+
>
45+
<div>Start New Chat</div>
46+
</div>
1447
{ tickets.map(elem => {
1548
const { title, uid } = elem;
1649
return (

src/components/chat/index.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState } from "react";
1+
import { useState } from "react";
22
import Tickets from "./Tickets";
33
import Conversation from "./Conversation";
44

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Link } from "react-router-dom"
2+
3+
export default function SidebarIcon({to, children, pathname}) {
4+
5+
return (
6+
<Link to={to} className={`item${pathname===to?' selected':''}`}>
7+
{ children }
8+
</Link>
9+
)
10+
}

src/components/sidebar/index.jsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Outlet, useLocation } from "react-router-dom";
2+
import { Chat, Gear } from "react-bootstrap-icons";
3+
import SidebarIcon from "./SidebarIcons";
4+
5+
export default function Sidebar() {
6+
const { pathname } = useLocation();
7+
8+
return (
9+
<div className="main">
10+
<div className="app-sidebar">
11+
<div className="section">
12+
<SidebarIcon to={'/chat'} pathname={pathname}><Chat/></SidebarIcon>
13+
</div>
14+
<div className="section bottom">
15+
<SidebarIcon to={'/settings'} pathname={pathname}><Gear/></SidebarIcon>
16+
</div>
17+
</div>
18+
<div className="window">
19+
<Outlet />
20+
</div>
21+
</div>
22+
)
23+
}

0 commit comments

Comments
 (0)