diff --git a/package.json b/package.json index c2989ea..6ae7200 100644 --- a/package.json +++ b/package.json @@ -47,8 +47,7 @@ "type": "webview", "id": "firecoder.chat-gui", "name": "", - "visibility": "visible", - "when": "config.firecoder.experimental.chat || config.firecoder.cloud.use" + "visibility": "visible" } ] }, diff --git a/src/common/auth/supabaseClient.ts b/src/common/auth/supabaseClient.ts index c0c26c0..285e915 100644 --- a/src/common/auth/supabaseClient.ts +++ b/src/common/auth/supabaseClient.ts @@ -16,7 +16,6 @@ export const getSuppabaseClient = () => { autoRefreshToken: true, persistSession: true, detectSessionInUrl: false, - debug: true, storage: secretsStorage, }, }); diff --git a/src/common/panel/chat.ts b/src/common/panel/chat.ts index 68b9464..b1a15ce 100644 --- a/src/common/panel/chat.ts +++ b/src/common/panel/chat.ts @@ -5,6 +5,9 @@ import { getNonce } from "../utils/getNonce"; import { chat } from "../chat"; import { Chat, ChatMessage } from "../prompt/promptChat"; import { state } from "../utils/state"; +import { configuration } from "../utils/configuration"; +import { TypeModelsChat, modelsChat, servers } from "../server"; +import { getSuppabaseClient } from "../auth/supabaseClient"; export type MessageType = | { @@ -29,6 +32,12 @@ type MessageToExtention = type: "abort-generate"; id: string; } + | { + type: "get-settings"; + } + | { + type: "enable-chat"; + } | { type: "get-chat"; chatId: string; @@ -173,6 +182,16 @@ export class ChatPanel implements vscode.WebviewViewProvider { id: message.id, }); break; + case "get-settings": + await this.handleGetSettings({ + id: message.id, + }); + break; + case "enable-chat": + await this.handleEnableChat({ + id: message.id, + }); + break; default: break; } @@ -213,6 +232,40 @@ export class ChatPanel implements vscode.WebviewViewProvider { sendResponse("", true); } + private async handleGetSettings({ id }: { id: string }) { + const settigns = await this.getSettings(); + + await this.postMessage({ + type: "e2w-response", + id: id, + data: settigns, + done: true, + }); + } + + private async getSettings() { + const cloudUsing = configuration.get("cloud.use"); + const cloudChatUsing = configuration.get("cloud.chat.use"); + const chatServerIsWorking = Object.keys(modelsChat) + .map( + (chatModel) => servers[chatModel as TypeModelsChat].status === "started" + ) + .some((serverIsWorking) => serverIsWorking); + + const localChatUsing = configuration.get("experimental.chat"); + const supabase = getSuppabaseClient(); + const sesssion = await supabase.auth.getSession(); + const userLoggined = sesssion.data.session ? true : false; + + const chatEnabled = + (localChatUsing && chatServerIsWorking) || (cloudUsing && cloudChatUsing); + + return { + chatEnabled: chatEnabled, + userLoggined: userLoggined, + }; + } + private async handleGetChat({ chatId, id }: { chatId: string; id: string }) { const sendResponse = (messageToResponse: Chat | null, done: boolean) => { this.postMessage({ @@ -270,7 +323,17 @@ export class ChatPanel implements vscode.WebviewViewProvider { await this.postMessage({ type: "e2w-response", id: id, - data: "", + data: true, + done: true, + }); + } + + private async handleEnableChat({ id }: { id: string }) { + await configuration.set("experimental.chat", true); + await this.postMessage({ + type: "e2w-response", + id: id, + data: true, done: true, }); } diff --git a/src/common/server/index.ts b/src/common/server/index.ts index 2f42331..41ac32f 100644 --- a/src/common/server/index.ts +++ b/src/common/server/index.ts @@ -17,7 +17,7 @@ const modelsBase = { }; export type TypeModelsBase = keyof typeof modelsBase; -const modelsChat = { +export const modelsChat = { "chat-small": { port: 39725, }, @@ -197,7 +197,7 @@ class Server { }); const isServerStarted = await this.checkServerStatusIntervalWithTimeout( - 1000000 + 20000 ); if (!isServerStarted) { @@ -217,7 +217,7 @@ class Server { return true; } - public async stopServer() { + public stopServer() { if (this.serverProcess) { const result = this.serverProcess.kill(9); if (result === false) { diff --git a/src/common/utils/configuration.ts b/src/common/utils/configuration.ts index c70d2a6..d590116 100644 --- a/src/common/utils/configuration.ts +++ b/src/common/utils/configuration.ts @@ -87,11 +87,6 @@ interface ConfigurationPropertiesType } class Configuration { - // private configuration: vscode.WorkspaceConfiguration; - // constructor() { - // this.configuration = vscode.workspace.getConfiguration("firecoder"); - // } - public get( property: T ): ConfigurationPropertiesType[T]["possibleValues"] { @@ -100,6 +95,14 @@ class Configuration { return value ?? ConfigurationProperties[property]["default"]; } + + public async set( + property: T, + value: ConfigurationPropertiesType[T]["possibleValues"] + ) { + const configuration = vscode.workspace.getConfiguration("firecoder"); + await configuration.update(property, value, true); + } } export const configuration = new Configuration(); diff --git a/src/extension.ts b/src/extension.ts index c405e94..17aa3d0 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -70,18 +70,13 @@ export async function activate(context: vscode.ExtensionContext) { vscode.workspace.onDidChangeConfiguration(async (event) => { if (event.affectsConfiguration("firecoder.cloud.use")) { const cloudUse = configuration.get("cloud.use"); + const supabase = getSuppabaseClient(); if (cloudUse === true) { - const supabase = getSuppabaseClient(); - const data = await supabase.auth.getUser(); if (data.error) { await login(); return; } - } else { - const supabase = getSuppabaseClient(); - - await supabase.auth.signOut(); } } }); @@ -94,87 +89,67 @@ export async function activate(context: vscode.ExtensionContext) { }) ); - vscode.workspace.onDidChangeConfiguration(async (event) => { - if ( - event.affectsConfiguration("firecoder.cloud.use") || - event.affectsConfiguration("firecoder.experimental.chat") || - event.affectsConfiguration("firecoder.completion.manuallyMode") || - event.affectsConfiguration("firecoder.completion.autoMode") || - event.affectsConfiguration( - "firecoder.experimental.useGpu.linux.nvidia" - ) || - event.affectsConfiguration("firecoder.experimental.useGpu.osx.metal") || - event.affectsConfiguration( - "firecoder.experimental.useGpu.windows.nvidia" - ) || - event.affectsConfiguration("firecoder.server.usePreRelease") || - event.affectsConfiguration("firecoder.cloud.use.chat") || - event.affectsConfiguration("firecoder.cloud.use.autocomplete") - ) { - Object.values(servers).forEach((server) => server.stopServer()); - - const completionServers = - configuration.get("cloud.use") && - configuration.get("cloud.autocomplete.use") - ? [] - : new Set([ - configuration.get("completion.autoMode"), - configuration.get("completion.manuallyMode"), - ]); - const serversToStart = [ - ...completionServers, - ...(configuration.get("experimental.chat") && - configuration.get("cloud.use") && - configuration.get("cloud.chat.use") - ? [] - : ["chat-medium" as const]), - ]; - await Promise.all( - serversToStart.map((serverType) => servers[serverType].startServer()) - ); + const startChat = async () => { + if (configuration.get("cloud.use") && configuration.get("cloud.chat.use")) { + Logger.info("Use cloud for chat.", { + component: "main", + sendTelemetry: true, + }); + } else if (configuration.get("experimental.chat")) { + Logger.info("Use local for chat.", { + component: "main", + sendTelemetry: true, + }); + try { + await servers["chat-medium"].startServer(); + } catch (error) { + vscode.window.showErrorMessage((error as Error).message); + Logger.error(error, { + component: "server", + sendTelemetry: true, + }); + } + Logger.info("Chat is ready to start.", { + component: "main", + sendTelemetry: true, + }); + } else { + Logger.info("Chat is not enable", { + component: "main", + sendTelemetry: true, + }); } - }); + }; - (async () => { - if (configuration.get("cloud.use")) { - Logger.info("Use cloud for chat and completions", { + const startCompletion = async (registerCompletionProvider: boolean) => { + if ( + configuration.get("cloud.use") && + configuration.get("cloud.autocomplete.use") + ) { + Logger.info("Use cloud for auto completions.", { component: "main", sendTelemetry: true, }); - - const InlineCompletionProvider = getInlineCompletionProvider(context); - vscode.languages.registerInlineCompletionItemProvider( - { pattern: "**" }, - InlineCompletionProvider - ); } else { + Logger.info("Use local for auto completions.", { + component: "main", + sendTelemetry: true, + }); try { - const completionServers = configuration.get("cloud.use") - ? [] - : new Set([ - configuration.get("completion.autoMode"), - configuration.get("completion.manuallyMode"), - ]); const serversStarted = await Promise.all( [ - ...completionServers, - ...(configuration.get("experimental.chat") && - !configuration.get("cloud.use") - ? ["chat-medium" as const] - : []), + ...new Set([ + configuration.get("completion.autoMode"), + configuration.get("completion.manuallyMode"), + ]), ].map((serverType) => servers[serverType].startServer()) ); if (serversStarted.some((serverStarted) => serverStarted)) { - Logger.info("Server inited", { + Logger.info("Servers inited", { component: "main", sendTelemetry: true, }); - const InlineCompletionProvider = getInlineCompletionProvider(context); - vscode.languages.registerInlineCompletionItemProvider( - { pattern: "**" }, - InlineCompletionProvider - ); } } catch (error) { vscode.window.showErrorMessage((error as Error).message); @@ -184,12 +159,45 @@ export async function activate(context: vscode.ExtensionContext) { }); } } + if (registerCompletionProvider) { + const InlineCompletionProvider = getInlineCompletionProvider(context); + vscode.languages.registerInlineCompletionItemProvider( + { pattern: "**" }, + InlineCompletionProvider + ); + } + }; + + (async () => { + await Promise.all([startChat(), startCompletion(true)]); Logger.info("FireCoder is ready.", { component: "main", sendTelemetry: true, }); })(); + + vscode.workspace.onDidChangeConfiguration(async (event) => { + if ( + event.affectsConfiguration("firecoder.cloud.use") || + event.affectsConfiguration("firecoder.cloud.chat.use") || + event.affectsConfiguration("firecoder.cloud.autocomplete.use") || + event.affectsConfiguration("firecoder.experimental.chat") || + event.affectsConfiguration("firecoder.completion.manuallyMode") || + event.affectsConfiguration("firecoder.completion.autoMode") || + event.affectsConfiguration( + "firecoder.experimental.useGpu.linux.nvidia" + ) || + event.affectsConfiguration("firecoder.experimental.useGpu.osx.metal") || + event.affectsConfiguration( + "firecoder.experimental.useGpu.windows.nvidia" + ) || + event.affectsConfiguration("firecoder.server.usePreRelease") + ) { + Object.values(servers).forEach((server) => server.stopServer()); + await Promise.all([startChat(), startCompletion(false)]); + } + }); } export function deactivate() { diff --git a/webviews/src/hooks/useSettings.tsx b/webviews/src/hooks/useSettings.tsx new file mode 100644 index 0000000..fd1879c --- /dev/null +++ b/webviews/src/hooks/useSettings.tsx @@ -0,0 +1,62 @@ +import { createContext, useContext, useEffect, useMemo, useState } from "react"; +import { vscode } from "../utilities/vscode"; + +type ConfigurationType = { + chatEnabled: boolean; + userLoggined: boolean; +}; + +interface SettingsContextType { + configuration: ConfigurationType; +} + +const SettingsContext = createContext(null!); + +export const SettingsProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const [configuration, setConfiguration] = useState(null!); + + useEffect(() => { + let lastSettings: any = null; + + const getSettings = async () => { + const settings = await vscode.getSettings(); + if ( + settings.chatEnabled !== lastSettings?.chatEnabled || + settings.userLoggined !== lastSettings?.userLoggined + ) { + lastSettings = settings; + + setConfiguration({ + chatEnabled: settings.chatEnabled, + userLoggined: settings.userLoggined, + }); + } + }; + getSettings(); + + const interval = setInterval(getSettings, 5000); + return () => { + clearInterval(interval); + }; + }, []); + + const value = useMemo(() => ({ configuration }), [configuration]); + + if (configuration === null) { + return null; + } + + return ( + + {children} + + ); +}; + +export const useSettings = () => { + return useContext(SettingsContext); +}; diff --git a/webviews/src/index.tsx b/webviews/src/index.tsx index f637539..b8f3664 100644 --- a/webviews/src/index.tsx +++ b/webviews/src/index.tsx @@ -2,30 +2,42 @@ import React from "react"; import ReactDOM from "react-dom"; import { createMemoryRouter, RouterProvider } from "react-router-dom"; import "./index.css"; -import Root from "./routes/root/root"; +import Root from "./routes/root"; import { ChatInstance } from "./routes/chat"; import ChatsHistory, { loader as ChatsHistoryLoader, } from "./routes/chatsHistory"; +import Init from "./routes/init"; +import { SettingsProvider } from "./hooks/useSettings"; +import { RequireInit } from "./routes/requireInit"; const router = createMemoryRouter( [ { - path: "/", element: , children: [ { - path: "chats", - element: , - loader: ChatsHistoryLoader, + path: "/init", + element: , }, { - path: "chats/new-chat", - element: , - }, - { - path: "chats/:chatId", - element: , + path: "/chats", + element: , + children: [ + { + path: "/chats/history", + element: , + loader: ChatsHistoryLoader, + }, + { + path: "/chats/new-chat", + element: , + }, + { + path: "/chats/:chatId", + element: , + }, + ], }, ], }, @@ -37,7 +49,9 @@ const router = createMemoryRouter( ReactDOM.render( - + + + , document.getElementById("root") ); diff --git a/webviews/src/routes/chat/index.tsx b/webviews/src/routes/chat/index.tsx index f1c2a81..2730a1d 100644 --- a/webviews/src/routes/chat/index.tsx +++ b/webviews/src/routes/chat/index.tsx @@ -30,7 +30,10 @@ export const ChatInstance = () => { return (
- navigate("/chats")}> + navigate("/chats/history")} + >
diff --git a/webviews/src/routes/chatsHistory/index.tsx b/webviews/src/routes/chatsHistory/index.tsx index 103e97e..63e0536 100644 --- a/webviews/src/routes/chatsHistory/index.tsx +++ b/webviews/src/routes/chatsHistory/index.tsx @@ -1,5 +1,4 @@ import { useLoaderData, useNavigate } from "react-router-dom"; -import { Chat } from "../../hooks/useChat"; import { vscode } from "../../utilities/vscode"; import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; import styles from "./style.module.css"; @@ -9,8 +8,10 @@ export async function loader() { return chats; } +type LoaderReturn = Awaited>; + const ChatsHistory = () => { - const chats = useLoaderData() as Chat[]; + const chats = useLoaderData() as LoaderReturn; const navigate = useNavigate(); diff --git a/webviews/src/routes/init/index.tsx b/webviews/src/routes/init/index.tsx new file mode 100644 index 0000000..fd812a0 --- /dev/null +++ b/webviews/src/routes/init/index.tsx @@ -0,0 +1,51 @@ +import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; +import styles from "./style.module.css"; +import { vscode } from "../../utilities/vscode"; +import { useState } from "react"; +import { useSettings } from "../../hooks/useSettings"; +import { Navigate } from "react-router-dom"; + +export default function Init() { + const [isChatEnabled, setIsChatEnabled] = useState(false); + const settings = useSettings(); + + if (settings.configuration.chatEnabled) { + return ; + } + + return ( +
+

+ FireCoder Chat is currently disabled. Please enable it to start + chatting. +

+

+ FireCoder needs to download the chat model and save it to your device's + local storage. +
+ This model is quite large, around 6GB, so the download may take a few + minutes. +

+ { + setIsChatEnabled(true); + vscode.enableChat(); + }} + > + Enable + {isChatEnabled ? ( + + ) : ( + + )} + +
+ ); +} diff --git a/webviews/src/routes/init/style.module.css b/webviews/src/routes/init/style.module.css new file mode 100644 index 0000000..d165127 --- /dev/null +++ b/webviews/src/routes/init/style.module.css @@ -0,0 +1,7 @@ +.init { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 16px; +} diff --git a/webviews/src/routes/requireInit/index.tsx b/webviews/src/routes/requireInit/index.tsx new file mode 100644 index 0000000..4f34b77 --- /dev/null +++ b/webviews/src/routes/requireInit/index.tsx @@ -0,0 +1,12 @@ +import { Navigate, Outlet } from "react-router-dom"; +import { useSettings } from "../../hooks/useSettings"; + +export function RequireInit() { + let settings = useSettings(); + + if (!settings.configuration.chatEnabled) { + return ; + } + + return ; +} diff --git a/webviews/src/routes/root/root.tsx b/webviews/src/routes/root/index.tsx similarity index 100% rename from webviews/src/routes/root/root.tsx rename to webviews/src/routes/root/index.tsx diff --git a/webviews/src/utilities/vscode.ts b/webviews/src/utilities/vscode.ts index 7b63b47..d702fcc 100644 --- a/webviews/src/utilities/vscode.ts +++ b/webviews/src/utilities/vscode.ts @@ -26,6 +26,12 @@ type MessageToExtention = type: "abort-generate"; id: string; } + | { + type: "get-settings"; + } + | { + type: "enable-chat"; + } | { type: "get-chat"; chatId: string; @@ -154,6 +160,35 @@ class VSCodeAPIWrapper { }); } + public getSettings() { + return new Promise<{ + chatEnabled: boolean; + userLoggined: boolean; + }>((resolve) => { + this.postMessageCallback( + { + type: "get-settings", + }, + (message) => { + resolve(message.data); + } + ); + }); + } + + public enableChat() { + return new Promise((resolve) => { + this.postMessageCallback( + { + type: "enable-chat", + }, + (message) => { + resolve(message.data); + } + ); + }); + } + public saveChatHistory(chatId: string, history: Chat) { return new Promise((resolve) => { this.postMessageCallback(