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

fix(vscode): chat panel display issues #2276

Merged
merged 13 commits into from
May 29, 2024
1 change: 1 addition & 0 deletions clients/vscode/.gitattributes
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
*.png filter=lfs diff=lfs merge=lfs -text
*.svg filter=lfs diff=lfs merge=lfs -text
31 changes: 31 additions & 0 deletions clients/vscode/assets/chat-panel.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,34 @@ iframe {
width: 100%;
height: 100vh;
}

/* Static content page */
.static-content {
padding: 0.65rem 1.2rem;
}

.static-content .avatar {
display: flex;
align-items: center;
}

.static-content .avatar img {
width: 1rem;
height: 1rem;
object-fit: contain;
border-radius: 100%;
margin-right: 0.4rem;
padding: 0.2rem;
border: 1px solid var(--vscode-editorWidget-border);
background-color: rgb(232, 226, 210);
}

.static-content .title {
margin: 0.45rem 0 0;
font-size: 0.85rem;
}

.static-content p {
line-height: 1.45;
margin: 0.45rem 0;
}
4 changes: 3 additions & 1 deletion clients/vscode/assets/side-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions clients/vscode/assets/tabby.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
128 changes: 88 additions & 40 deletions clients/vscode/src/chat/ChatViewProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export class ChatViewProvider implements WebviewViewProvider {
webview?: WebviewView;
client?: ServerApi;
private pendingMessages: ChatMessage[] = [];
private isReady = false;

constructor(
private readonly context: ExtensionContext,
Expand All @@ -19,27 +20,19 @@ export class ChatViewProvider implements WebviewViewProvider {

public async resolveWebviewView(webviewView: WebviewView) {
this.webview = webviewView;
this.isReady = this.agent.status === "ready";
const extensionUri = this.context.extensionUri;

webviewView.webview.options = {
enableScripts: true,
localResourceRoots: [extensionUri],
};
// FIXME: we need to wait for the server to be ready, consider rendering a loading indicator
if (this.agent.status !== "ready") {
await new Promise<void>((resolve) => {
this.agent.on("didChangeStatus", (status) => {
if (status === "ready") {
resolve();
}
});
});

if (this.isReady) {
await this.renderChatPage();
} else {
webviewView.webview.html = this.getWelcomeContent();
}
const serverInfo = await this.agent.fetchServerInfo();
webviewView.webview.html = this.getWebviewContent(serverInfo);
this.agent.on("didUpdateServerInfo", (serverInfo: ServerInfo) => {
webviewView.webview.html = this.getWebviewContent(serverInfo);
});

this.client = createClient(webviewView, {
navigate: async (context: Context) => {
Expand All @@ -50,18 +43,27 @@ export class ChatViewProvider implements WebviewViewProvider {
},
});

this.agent.on("didChangeStatus", async (status) => {
if (status === "ready" && !this.isReady) {
this.isReady = true;
await this.renderChatPage();
}
});

this.agent.on("didUpdateServerInfo", async (serverInfo: ServerInfo) => {
await this.renderChatPage(serverInfo);
});

// The event will not be triggered during the initial rendering.
webviewView.onDidChangeVisibility(async () => {
if (webviewView.visible) {
await this.initChatPage();
}
});

webviewView.webview.onDidReceiveMessage(async (message) => {
if (message.action === "rendered") {
this.webview?.webview.postMessage({ action: "sync-theme" });
this.pendingMessages.forEach((message) => this.client?.sendMessage(message));
const serverInfo = await this.agent.fetchServerInfo();
if (serverInfo.config.token) {
this.client?.init({
fetcherOptions: {
authorization: serverInfo.config.token,
},
});
}
await this.initChatPage();
}
});

Expand All @@ -72,6 +74,15 @@ export class ChatViewProvider implements WebviewViewProvider {
});
}

private async renderChatPage(serverInfo?: ServerInfo) {
if (!serverInfo) {
serverInfo = await this.agent.fetchServerInfo();
}
if (this.webview) {
this.webview.webview.html = this.getWebviewContent(serverInfo);
}
}

private isChatPanelAvailable(serverInfo: ServerInfo): boolean {
if (!serverInfo.health || !serverInfo.health["webserver"] || !serverInfo.health["chat_model"]) {
return false;
Expand All @@ -94,24 +105,28 @@ export class ChatViewProvider implements WebviewViewProvider {
return true;
}

private async initChatPage() {
this.webview?.webview.postMessage({ action: "sync-theme" });
this.pendingMessages.forEach((message) => this.client?.sendMessage(message));
const serverInfo = await this.agent.fetchServerInfo();
if (serverInfo.config.token) {
this.client?.init({
fetcherOptions: {
authorization: serverInfo.config.token,
},
});
}
}

private getWebviewContent(serverInfo: ServerInfo) {
if (!this.isChatPanelAvailable(serverInfo)) {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
</head>
<body>
<h2>Tabby is not available</h2>
<ul>
<li>Please update to <a href="https://github.com/TabbyML/tabby/releases" target="_blank">the latest version</a> of the Tabby server.</li>
<li>You also need to launch the server with the chat model enabled; for example, use <code>--chat-model Mistral-7B</code>.</li>
</ul>
</body>
</html>
`;
return this.getStaticContent(`
<h4 class='title'>Tabby is not available</h4>
<p>Please update to <a href="https://github.com/TabbyML/tabby/releases" target="_blank">the latest version</a> of the Tabby server.</p>
<p>You also need to launch the server with the chat model enabled; for example, use <code>--chat-model Mistral-7B</code>.</p>
`);
}

const endpoint = serverInfo.config.endpoint;
const styleUri = this.webview?.webview.asWebviewUri(
Uri.joinPath(this.context.extensionUri, "assets", "chat-panel.css"),
Expand All @@ -123,7 +138,6 @@ export class ChatViewProvider implements WebviewViewProvider {
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tabby</title>
<link href="${endpoint}" rel="preconnect">
<link href="${styleUri}" rel="stylesheet">
<script defer>
Expand Down Expand Up @@ -172,6 +186,40 @@ export class ChatViewProvider implements WebviewViewProvider {
`;
}

// The content is displayed before the server is ready
private getWelcomeContent() {
return this.getStaticContent(`
<h4 class='title'>Welcome to Tabby Chat!</h4>
<p>Before you can start chatting, please take a moment to set up your credentials to connect to the Tabby server.</p>
`);
}

private getStaticContent(htmlContent: string) {
const logoUri = this.webview?.webview.asWebviewUri(Uri.joinPath(this.context.extensionUri, "assets", "tabby.png"));
const styleUri = this.webview?.webview.asWebviewUri(
Uri.joinPath(this.context.extensionUri, "assets", "chat-panel.css"),
);
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="${styleUri}" rel="stylesheet">
</head>
<body>
<main class='static-content'>
<div class='avatar'>
<img src="${logoUri}" />
<p>Tabby</p>
</div>
${htmlContent}
</main>
</body>
</html>
`;
}

public getWebview() {
return this.webview;
}
Expand Down