Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chat panel #155

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions jupyter_collaboration/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,20 +255,25 @@ async def on_message(self, message):
return skip

if message_type == MessageType.CHAT:
msg = message[2:].decode("utf-8")

user = self.current_user
data = json.dumps(
{"sender": user.username, "timestamp": time.time(), "content": json.loads(msg)}
).encode("utf8")

# The fist bytes are the length of the message
pos = 1
while message[pos] >= 128:
pos += 1
pos += 1
msg = message[pos:].decode("utf-8")

data = json.loads(msg)
data["sender"] = user.username
data["server_ts"] = time.time()
data = json.dumps(data).encode("utf8")

for client in self.room.clients:
if client != self:
task = asyncio.create_task(
self.create_task(
client.send(bytes([MessageType.CHAT]) + write_var_uint(len(data)) + data)
)
self._websocket_server.background_tasks.add(task)
task.add_done_callback(self._websocket_server.background_tasks.discard)

self._message_queue.put_nowait(message)
self._websocket_server.ypatch_nb += 1
Expand Down
3 changes: 3 additions & 0 deletions packages/chat/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# @jupyter/chat

A JupyterLab package which provides a set of widgets for Chat communication.
62 changes: 62 additions & 0 deletions packages/chat/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
"name": "@jupyter/chat",
"version": "1.0.0-alpha.8",
"description": "JupyterLab - Chat Widgets",
"homepage": "https://github.com/jupyterlab/jupyter_collaboration",
"bugs": {
"url": "https://github.com/jupyterlab/jupyter_collaboration/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/jupyterlab/jupyter-collaboration.git"
},
"license": "BSD-3-Clause",
"author": "Project Jupyter",
"sideEffects": [
"style/*.css",
"style/index.js"
],
"main": "lib/index.js",
"types": "lib/index.d.ts",
"style": "style/index.css",
"directories": {
"lib": "lib/"
},
"files": [
"lib/*.d.ts",
"lib/*.js.map",
"lib/*.js",
"style/*.css",
"style/index.js"
],
"scripts": {
"build": "tsc -b",
"build:prod": "jlpm run build",
"clean": "rimraf lib tsconfig.tsbuildinfo",
"clean:lib": "jlpm run clean:all",
"clean:all": "rimraf lib tsconfig.tsbuildinfo node_modules",
"install:extension": "jlpm run build",
"watch": "tsc -b --watch"
},
"dependencies": {
"@jupyter/docprovider": "^2.0.1",
"@jupyterlab/services": "^7.0.5",
"@jupyterlab/translation": "^4.0.5",
"@jupyterlab/ui-components": "^4.0.5",
"@lumino/widgets": "^2.1.0"
},
"devDependencies": {
"rimraf": "^4.1.2",
"typescript": "~5.0.4"
},
"publishConfig": {
"access": "public"
},
"typedoc": {
"entryPoint": "./src/index.ts",
"readmeFile": "./README.md",
"displayName": "@jupyter/chat",
"tsconfig": "./tsconfig.json"
},
"styleModule": "style/index.js"
}
207 changes: 207 additions & 0 deletions packages/chat/src/chatpanel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

import { User } from '@jupyterlab/services';
import { ITranslator } from '@jupyterlab/translation';
import { LabIcon, SidePanel, caretRightIcon } from '@jupyterlab/ui-components';

import { Panel, Widget } from '@lumino/widgets';

import { IAwarenessProvider, IChatMessage } from '@jupyter/docprovider';

import chatSvgstr from '../style/icons/chat.svg';

/**
* The icon for the chat panel.
*/
export const chatIcon = new LabIcon({
name: 'collaboration:chat',
svgstr: chatSvgstr
});

/**
* The chat panel widget.
*/
export class ChatPanel extends SidePanel {
/**
* The constructor of the panel.
*/
constructor(options: ChatPanel.IOptions) {
super({ content: new Panel(), translator: options.translator });
this._user = options.currentUser;
this._provider = options.provider;
this.addClass('jp-ChatPanel');

this._messages.addClass('jp-ChatPanel-messages');
this.addWidget(this._messages);
this.addWidget(new Widget({ node: this._prompt }));

this._provider.messageStream.connect(this.onMessageReceived, this);
}

dispose(): void {
if (this.isDisposed) {
return;
}
this._provider.messageStream.disconnect(this.onMessageReceived, this);
super.dispose();
}

/**
* Add a new message in the list.
* @param messageContent - Content and metadata of the message.
*/
onMessageReceived(sender: IAwarenessProvider, message: IChatMessage): void {
let index = this._messages.widgets.length;
for (const msg of this._messages.widgets.slice(1).reverse()) {
if (new Date(message.timestamp) > (msg as ChatMessage).date) {
break;
}
index -= 1;
}

this._messages.insertWidget(index, new ChatMessage(message, this._user));
}

/**
* Send a new message.
* @param message - The message content.
*/
send = (message: string): void => {
if (!message) {
return;
}

const msg = this._provider.sendMessage(message);

this._messages.insertWidget(
this._messages.widgets.length,
new ChatMessage(msg, this._user)
);
};

/**
* Build the prompt div.
*/
private get _prompt(): HTMLDivElement {
const div = document.createElement('div');
div.classList.add('jp-ChatPanel-prompt');

const input = document.createElement('div');
input.role = 'textarea';
input.contentEditable = 'true';
input.onkeydown = (e: KeyboardEvent) => {
if (e.key === 'Enter' && e.ctrlKey && input.textContent) {
this.send(input.textContent);
input.textContent = '';
}
};

// input.setAttribute('placeholder', 'Send a message...');
// input.onclick = () => input.focus();

const buttonContainer = document.createElement('div');
buttonContainer.classList.add('jp-ChatPanel-promptButton');
const button = document.createElement('button');
const icon = LabIcon.resolveSvg(caretRightIcon);
if (icon) {
icon.style.width = '24px';
button.appendChild(icon);
} else {
const span = document.createElement('span');
span.innerText = 'Send';
button.appendChild(span);
}
button.classList.add('jp-mod-minimal');
button.classList.add('jp-Button');
button.onclick = () => {
if (input.textContent) {
this.send(input.textContent);
input.textContent = '';
}
};
buttonContainer.append(button);

div.append(input);
div.append(buttonContainer);
return div;
}

private _user: User.IManager;
private _messages = new Panel();
private _provider: IAwarenessProvider;
}

/**
* The message widget.
*/
class ChatMessage extends Widget {
/**
* The constructor of the message object.
* @param message - Content and metadata of the message.
* @param user - The current connected user.
*/
constructor(message: IChatMessage, user: User.IManager) {
super();
this._message = message;
this.addClass('jp-ChatPanel-message');
this.node.appendChild(this._header(user));
this.node.appendChild(this._content());
}

/**
* Get the date of the message.
*/
get date(): Date {
return new Date(this._message.timestamp);
}

/**
* Build the header of the message.
* @param currentUser - The current connected user.
* @returns - The div element containing the header.
*/
private _header(currentUser: User.IManager): HTMLDivElement {
const header = document.createElement('div');
const user = document.createElement('div');
user.innerText =
currentUser.identity?.username === this._message.sender.username
? 'You'
: this._message.sender.display_name || '???';
user.style.color = this._message.sender.color || 'inherit';
header.append(user);

const date = document.createElement('div');
date.classList.add('jp-ChatPanel-messageDate');
date.innerText = `${this.date.toLocaleDateString()} ${this.date.toLocaleTimeString()}`;
header.append(date);
return header;
}

/**
* Build the content of the message.
* @returns - The div element containing the message.
*/
private _content(): HTMLDivElement {
const message = document.createElement('div');
message.classList.add('jp-ChatPanel-messageContent');
message.innerText = this._message.content.body;
return message;
}

private _message: IChatMessage;
}

/**
* The chat panel namespace.
*/
export namespace ChatPanel {
/**
* Options to use when building the chat panel.
*/
export interface IOptions {
provider: IAwarenessProvider;
currentUser: User.IManager;
translator?: ITranslator;
}
}
9 changes: 9 additions & 0 deletions packages/chat/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/**
* @packageDocumentation
* @module chat
*/

// export * from './tokens';
export * from './chatpanel';
9 changes: 9 additions & 0 deletions packages/chat/src/svg.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright (c) Jupyter Development Team.
* Distributed under the terms of the Modified BSD License.
*/

declare module '*.svg' {
const value: string; // @ts-ignore
export default value;
}
2 changes: 2 additions & 0 deletions packages/chat/src/tokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
59 changes: 59 additions & 0 deletions packages/chat/style/base.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/* -----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified BSD License.
|---------------------------------------------------------------------------- */

.jp-ChatPanel .jp-SidePanel-content {
display: flex;
flex-direction: column;
padding: 5px 0;
}

.jp-ChatPanel-messages {
flex-grow: 1;
}

.jp-ChatPanel-message {
margin: 5px 2px;
}

.jp-ChatPanel-messageDate {
color: var(--jp-content-font-color3);
font-size: var(--jp-ui-font-size0);
}

.jp-ChatPanel-messageContent {
background: var(--jp-layout-color2);
padding: 3px;
border-radius: 5px;
display: inline-block;
}

.jp-ChatPanel-prompt {
display: flex;
width: 100%;
max-height: 20%;
}

.jp-ChatPanel-prompt div[role="textarea"] {
font-size: var(--jp-ui-font-size1);
flex-grow: 1;
resize: none;
overflow: auto;
border: solid 1px;
min-height: 1em;
padding: 3px;
margin-left: 3px;
border-radius: 5px;
}

/* .jp-ChatPanel-promptPlaceholder:empty:not(:focus):before {
content: attr(placeholder);
color: var(--jp-ui-font-color2);
font-style: italic;
} */

.jp-ChatPanel-promptButton {
display: flex;
align-items: flex-end;
}
Loading
Loading