init devika repo

This commit is contained in:
2024-07-01 22:49:56 +03:00
commit f0b94ab9bd
164 changed files with 8016 additions and 0 deletions

12
ui/.gitignore vendored Normal file
View File

@@ -0,0 +1,12 @@
.DS_Store
node_modules/
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
pnpm-lock.yaml
.lockb

1
ui/.npmrc Normal file
View File

@@ -0,0 +1 @@
engine-strict=true

BIN
ui/bun.lockb Executable file

Binary file not shown.

14
ui/components.json Normal file
View File

@@ -0,0 +1,14 @@
{
"$schema": "https://shadcn-svelte.com/schema.json",
"style": "default",
"tailwind": {
"config": "tailwind.config.js",
"css": "src/app.pcss",
"baseColor": "zinc"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils"
},
"typescript": false
}

40
ui/package.json Normal file
View File

@@ -0,0 +1,40 @@
{
"name": "ui",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"start": "vite build && vite preview",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"@monaco-editor/loader": "^1.4.0",
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"autoprefixer": "^10.4.16",
"monaco-editor": "^0.48.0",
"postcss": "^8.4.32",
"postcss-load-config": "^5.0.2",
"svelte": "^4.2.7",
"tailwindcss": "^3.4.3",
"vite": "^5.2.8",
"vite-plugin-wasm": "^3.3.0"
},
"type": "module",
"dependencies": {
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",
"bits-ui": "^0.21.2",
"clsx": "^2.1.0",
"dompurify": "^3.1.5",
"mode-watcher": "^0.3.0",
"paneforge": "^0.0.3",
"socket.io-client": "^4.7.5",
"svelte-sonner": "^0.3.21",
"tailwind-merge": "^2.2.2",
"tailwind-variants": "^0.2.1",
"tiktoken": "^1.0.13"
}
}

13
ui/postcss.config.cjs Normal file
View File

@@ -0,0 +1,13 @@
const tailwindcss = require("tailwindcss");
const autoprefixer = require("autoprefixer");
const config = {
plugins: [
//Some plugins, like tailwindcss/nesting, need to run before Tailwind,
tailwindcss(),
//But others, like autoprefixer, need to run after,
autoprefixer,
],
};
module.exports = config;

15
ui/src/app.html Normal file
View File

@@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" />
%sveltekit.head%
<title>Devika AI</title>
</head>
<body data-sveltekit-preload-data="hover" class="h-dvh">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

137
ui/src/app.pcss Normal file
View File

@@ -0,0 +1,137 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--primary: #2F3337;
--background: #ffffff;
--secondary: #f1f2f5;
--tertiary: #919AA3;
--foreground: #303438;
--foreground-invert: #ffffff;
--foreground-light: #9CA3AB;
--foreground-secondary: #9CA3AB;
--btn: #2F3337;
--btn-active: #000000;
--border: #E4E3E8;
--seperator: #E4E3E8;
--window-outline: #E4E3E8;
--browser-window-dots: #E4E3E8;
--browser-window-search: #F7F8FA;
--browser-window-ribbon: #ffffff;
--browser-window-foreground: #303438;
--browser-window-background: #F7F8FA;
--terminal-window-dots: #E4E3E8;
--terminal-window-ribbon: #ffffff;
--terminal-window-background: #F7F8FA;
--terminal-window-foreground: #313336;
--slider-empty: #9CA3AB;
--slider-filled: #2F3337;
--slider-thumb: #303438;
--monologue-background: #F1F3F5;
--monologue-outline: #B2BAC2;
}
.dark{
--primary: #ECECEC;
--background: #1D1F21;
--secondary: #2F3337;
--tertiary: #81878C;
--foreground: #dcdcdc;
--foreground-invert: #1D1F21;
--foreground-light: #E6E9EB;
--foreground-secondary: #9CA3AB;
--btn: #ECECEC;
--btn-active: #ffffff;
--border: #2B2F34;
--seperator: #495058;
--window-outline: #4E555D;
--browser-window-dots: #191C1E;
--browser-window-search: #1D2124;
--browser-window-ribbon: #292E32;
--browser-window-foreground: #DDDFE1;
--browser-window-background: #111315;
--terminal-window-dots: #191C1E;
--terminal-window-ribbon: #292E32;
--terminal-window-background: #111315;
--terminal-window-foreground: #9CA3AB;
--slider-empty: #2F3337;
--slider-filled: #81878C;
--slider-thumb: #ffffff;
--monologue-background: #242729;
--monologue-outline: #464C51;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
/* Styling for scrollbar */
/* WebKit (Chrome, Safari) */
*::-webkit-scrollbar {
width: 5px;
height: 2px
}
*::-webkit-scrollbar-thumb {
background: #999797;
border-radius: 0.5rem;
}
*::-webkit-scrollbar-thumb:hover {
background: #6b7280;
}
/* firefox */
@-moz-document url-prefix() {
:global(*) {
scrollbar-width: thin;
scrollbar-color: #999797 #FFFFFF;
}
:global(*::-moz-scrollbar) {
width: 5px;
}
:global(*::-moz-scrollbar-thumb) {
background: #999797;
border-radius: 0.5rem;
}
:global(*::-moz-scrollbar-thumb:hover) {
background: #6b7280;
}
}
/* Internet Explorer/Edge */
:global(*::-ms-scrollbar) {
width: 5px;
}
:global(*::-ms-scrollbar-thumb) {
background: #999797;
border-radius: 0.5rem;
}
:global(*::-ms-scrollbar-thumb:hover) {
background: #6b7280;
}
}

165
ui/src/lib/api.js Normal file
View File

@@ -0,0 +1,165 @@
import {
agentState,
internet,
modelList,
projectList,
messages,
projectFiles,
searchEngineList,
} from "./store";
import { io } from "socket.io-client";
const getApiBaseUrl = () => {
if (typeof window !== 'undefined') {
const host = window.location.hostname;
if (host === 'localhost' || host === '127.0.0.1') {
return 'http://127.0.0.1:1337';
} else {
return `http://${host}:1337`;
}
} else {
return 'http://127.0.0.1:1337';
}
};
export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || getApiBaseUrl();
export const socket = io(API_BASE_URL, { autoConnect: false });
export async function checkServerStatus() {
try{await fetch(`${API_BASE_URL}/api/status`) ; return true;}
catch (error) {
return false;
}
}
export async function fetchInitialData() {
const response = await fetch(`${API_BASE_URL}/api/data`);
const data = await response.json();
projectList.set(data.projects);
modelList.set(data.models);
searchEngineList.set(data.search_engines);
localStorage.setItem("defaultData", JSON.stringify(data));
}
export async function createProject(projectName) {
await fetch(`${API_BASE_URL}/api/create-project`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ project_name: projectName }),
});
projectList.update((projects) => [...projects, projectName]);
}
export async function deleteProject(projectName) {
await fetch(`${API_BASE_URL}/api/delete-project`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ project_name: projectName }),
});
}
export async function fetchMessages() {
const projectName = localStorage.getItem("selectedProject");
const response = await fetch(`${API_BASE_URL}/api/messages`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ project_name: projectName }),
});
const data = await response.json();
messages.set(data.messages);
}
export async function fetchAgentState() {
const projectName = localStorage.getItem("selectedProject");
const response = await fetch(`${API_BASE_URL}/api/get-agent-state`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ project_name: projectName }),
});
const data = await response.json();
agentState.set(data.state);
}
export async function executeAgent(prompt) {
const projectName = localStorage.getItem("selectedProject");
const modelId = localStorage.getItem("selectedModel");
if (!modelId) {
alert("Please select the LLM model first.");
return;
}
await fetch(`${API_BASE_URL}/api/execute-agent`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
prompt: prompt,
base_model: modelId,
project_name: projectName,
}),
});
await fetchMessages();
}
export async function getBrowserSnapshot(snapshotPath) {
const response = await fetch(`${API_BASE_URL}/api/browser-snapshot`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ snapshot_path: snapshotPath }),
});
const data = await response.json();
return data.snapshot;
}
export async function fetchProjectFiles() {
const projectName = localStorage.getItem("selectedProject");
const response = await fetch(`${API_BASE_URL}/api/get-project-files?project_name=${projectName}`)
const data = await response.json();
projectFiles.set(data.files);
return data.files;
}
export async function checkInternetStatus() {
if (navigator.onLine) {
internet.set(true);
} else {
internet.set(false);
}
}
export async function fetchSettings() {
const response = await fetch(`${API_BASE_URL}/api/settings`);
const data = await response.json();
return data.settings;
}
export async function updateSettings(settings) {
await fetch(`${API_BASE_URL}/api/settings`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(settings),
});
}
export async function fetchLogs() {
const response = await fetch(`${API_BASE_URL}/api/logs`);
const data = await response.json();
return data.logs;
}

View File

@@ -0,0 +1,50 @@
<script>
import { agentState } from "$lib/store";
import { API_BASE_URL, socket } from "$lib/api";
socket.on('screenshot', function(msg) {
const data = msg['data'];
const img = document.querySelector('.browser-img');
img.src = `data:image/png;base64,${data}`;
});
</script>
<div class="w-full h-full flex flex-col border-[3px] rounded-xl overflow-y-auto bg-browser-window-background border-window-outline">
<div class="p-2 flex items-center border-b border-border bg-browser-window-ribbon h-12">
<div class="flex space-x-2 ml-2 mr-4">
<div class="w-3 h-3 bg-browser-window-dots rounded-full"></div>
<div class="w-3 h-3 bg-browser-window-dots rounded-full"></div>
<div class="w-3 h-3 bg-browser-window-dots rounded-full"></div>
</div>
<input
type="text"
id="browser-url"
class="flex-grow h-7 text-xs rounded-lg p-2 overflow-x-auto bg-browser-window-search text-browser-window-foreground"
placeholder="devika://newtab"
value={$agentState?.browser_session.url || ""}
/>
</div>
<div id="browser-content" class="flex-grow overflow-y-auto">
{#if $agentState?.browser_session.screenshot}
<img
class="browser-img"
src={API_BASE_URL + "/api/get-browser-snapshot?snapshot_path=" + $agentState?.browser_session.screenshot}
alt="Browser snapshot"
/>
{:else}
<div class="text-gray-400 text-sm text-center mt-5"><strong>💡 TIP:</strong> You can include a Git URL in your prompt to clone a repo!</div>
{/if}
</div>
</div>
<style>
#browser-url {
pointer-events: none
}
.browser-img {
display: block;
object-fit: contain;
}
</style>

View File

@@ -0,0 +1,300 @@
<script>
import { onMount } from "svelte";
import { projectList, modelList, internet, tokenUsage, agentState, messages, searchEngineList, serverStatus, isSending, selectedProject, selectedModel, selectedSearchEngine} from "$lib/store";
import { createProject, fetchMessages, fetchInitialData, deleteProject,fetchProjectFiles, fetchAgentState} from "$lib/api";
import Seperator from "./ui/Seperator.svelte";
function selectProject(project) {
$selectedProject = project;
fetchMessages();
fetchAgentState();
fetchProjectFiles();
document.getElementById("project-dropdown").classList.add("hidden");
}
function selectModel(model) {
$selectedModel = model;
document.getElementById("model-dropdown").classList.add("hidden");
}
function selectSearchEngine(searchEngine) {
$selectedSearchEngine = searchEngine;
document.getElementById("search-engine-dropdown").classList.add("hidden");
}
async function createNewProject() {
const projectName = prompt('Enter the project name:');
if (projectName) {
await createProject(projectName);
selectProject(projectName);
tokenUsage.set(0);
messages.set([]);
agentState.set(null);
isSending.set(false);
}
}
async function deleteproject(project) {
if (confirm(`Are you sure you want to delete ${project}?`)) {
await deleteProject(project);
await fetchInitialData();
messages.set([]);
agentState.set(null);
tokenUsage.set(0);
isSending.set(false);
$selectedProject = "Select Project";
localStorage.setItem("selectedProject", "");
}
}
const dropdowns = [
{ dropdown: "project-dropdown", button: "project-button" },
{ dropdown: "model-dropdown", button: "model-button" },
{ dropdown: "search-engine-dropdown", button: "search-engine-button" },
];
function closeDropdowns(event) {
dropdowns.forEach(({ dropdown, button }) => {
const dropdownElement = document.getElementById(dropdown);
const buttonElement = document.getElementById(button);
if (
dropdownElement &&
buttonElement &&
!dropdownElement.contains(event.target) &&
!buttonElement.contains(event.target)
) {
dropdownElement.classList.add("hidden");
}
});
}
onMount(() => {
(async () => {
if(serverStatus){
await fetchInitialData();
}
})();
dropdowns.forEach(({ dropdown, button }) => {
document.getElementById(button).addEventListener("click", function () {
const dropdownElement = document.getElementById(dropdown);
dropdownElement.classList.toggle("hidden");
});
});
document.addEventListener("click", closeDropdowns);
return () => {
document.removeEventListener("click", closeDropdowns);
};
});
</script>
<div class="control-panel border-b border-border bg-background pb-3">
<div class="dropdown-menu relative inline-block">
<button
type="button"
class="inline-flex items-center justify-between w-full text-foreground h-10 gap-2 px-3 py-2 text-sm min-w-[200px] bg-secondary rounded-md"
id="project-button"
aria-expanded="true"
aria-haspopup="true"
>
<span id="selected-project">{$selectedProject}</span>
<i class="fas fa-angle-down text-tertiary"></i>
</button>
<div
id="project-dropdown"
class="absolute left-0 z-10 mt-2 min-w-[200px] origin-top-left rounded-xl bg-secondary shadow-lg max-h-96 overflow-y-auto hidden"
role="menu"
aria-orientation="vertical"
aria-labelledby="project-button"
tabindex="-1"
>
<div role="none" class="flex flex-col divide-y-2 w-full">
<button
class="flex gap-2 items-center text-sm px-4 py-3 w-full"
on:click|preventDefault={createNewProject}
>
<i class="fas fa-plus"></i>
new project
</button>
{#if $projectList !== null}
{#each $projectList as project}
<div
class="flex items-center px-4 hover:bg-black/20 transition-colors">
<button
href="#"
class="flex gap-2 items-center text-sm py-3 w-full h-full overflow-x-visible"
on:click|preventDefault={() => selectProject(project)}
>
{project}
</button>
<button
class="fa-regular fa-trash-can hover:text-red-600"
on:click={() => deleteproject(project)}
aria-label="Delete project"
></button>
</div>
{/each}
{/if}
</div>
</div>
</div>
<div
class=""
style="display: flex; align-items: center; gap: 20px"
>
<div class="flex items-center gap-2 text-sm">
<span>Internet:</span>
<span class=" size-3 rounded-full" class:online={$internet} class:offline={!$internet}></span>
</div>
<Seperator />
<div class="flex items-center gap-2 text-sm">
<span>Token Usage:</span>
<span id="token-count" class="token-count-animation text-foreground">{$tokenUsage}</span>
</div>
<div class="relative inline-block text-left">
<div>
<button
type="button"
class="inline-flex items-center justify-between min-w-[200px] text-foreground w-fit gap-2 px-3 py-2 text-sm h-10 bg-secondary rounded-md"
id="search-engine-button"
aria-expanded="true"
aria-haspopup="true"
>
<span id="selected-search-engine">{$selectedSearchEngine}</span>
<i class="fas fa-angle-down text-tertiary"></i>
</button>
</div>
<div
id="search-engine-dropdown"
class="absolute left-0 z-10 mt-2 origin-top-right min-w-[200px] bg-secondary rounded-xl shadow-lg max-h-96 overflow-y-auto hidden"
role="menu"
aria-orientation="vertical"
aria-labelledby="search-engine-button"
tabindex="-1"
>
<div role="none" class="flex flex-col divide-y-2 w-full">
{#if $searchEngineList !== null}
{#each $searchEngineList as engine}
<div
class="flex items-center px-4 hover:bg-black/20 transition-colors
{selectSearchEngine === engine ? 'bg-gray-300' : ''}"
>
<button
class="flex gap-2 items-center text-sm py-3 w-full text-clip"
on:click|preventDefault={() => selectSearchEngine(engine)}
>
{engine}
</button>
</div>
{/each}
{/if}
</div>
</div>
</div>
<div class="relative inline-block text-left">
<div>
<button
type="button"
class="inline-flex items-center text-foreground justify-between w-fit gap-x-1.5 min-w-[150px] px-3 py-2 text-sm h-10 bg-secondary rounded-md"
id="model-button"
aria-expanded="true"
aria-haspopup="true"
>
<span id="selected-model">{$selectedModel}</span>
<i class="fas fa-angle-down text-tertiary"></i>
</button>
</div>
<div
id="model-dropdown"
class="absolute right-0 z-10 mt-2 w-64 origin-top-right bg-secondary rounded-xl shadow-lg max-h-96 overflow-y-auto hidden"
role="menu"
aria-orientation="vertical"
aria-labelledby="model-button"
tabindex="-1"
>
{#if $modelList !== null}
<div class="flex flex-col divide-y-2">
{#each Object.entries($modelList) as [modelName, modelItems]}
<div class="flex flex-col py-4 gap-2" role="none">
<span class="text-sm px-4 w-full font-semibold"
>{modelName.toLowerCase()}</span
>
<div class="flex flex-col gap-[1px] px-6 w-full">
{#each modelItems as models}
<button
class="relative nav-button flex text-start text-sm text-clip hover:bg-black/20 px-2 py-1.5 rounded-md transition-colors
{selectedModel == models[0] ? 'bg-gray-300': ''}"
on:click|preventDefault={() => selectModel(models[0])}
>
{models[0]}
<span class="tooltip text-[10px] px-2 text-gray-500"
>{models[1]}</span
>
</button>
{/each}
</div>
</div>
{/each}
</div>
{/if}
</div>
</div>
</div>
</div>
<style>
.tooltip {
font-size: 10px;
background-color: black;
color: white;
text-align: center;
border-radius: 100px;
padding: 5px 10px;
position: absolute;
z-index: 1;
opacity: 0;
top: -20px;
right: 0;
transition: opacity 0.3s;
}
.nav-button:hover .tooltip {
visibility: visible;
opacity: 1;
}
@keyframes roll {
0% {
transform: translateY(-5%);
}
100% {
transform: translateY(0);
}
}
.online {
background-color: #22c55e;
}
.offline {
background-color: #ef4444;
}
.token-count-animation {
display: inline-block;
animation: roll 0.5s ease-in-out;
}
.control-panel {
display: flex;
justify-content: space-between;
align-items: center;
}
.control-panel > *:not(:first-child) {
margin-left: 20px;
}
</style>

View File

@@ -0,0 +1,99 @@
<script>
import { onDestroy, onMount } from 'svelte';
import { initializeMonaco, initializeEditorRef, createModel, disposeEditor, enableTabSwitching, sidebar } from './MonacoEditor';
import { socket } from "$lib/api";
import { projectFiles } from "$lib/store";
let monaco;
let models = {};
let editor = null;
let editorContainer;
let tabContainer;
let sidebarContainer;
const reCreateEditor = async (files) => {
disposeEditor(editor);
models = {};
editor = await initializeEditorRef(monaco, editorContainer)
files.forEach((file) => {
let model = createModel(monaco, file);
editor.setModel(model);
models = {
...models,
[file.file]: model
};
});
enableTabSwitching(editor, models, tabContainer);
sidebar(editor, models, sidebarContainer);
};
const patchOrFeature = (files) => {
files.forEach((file, index) => {
const model = models[file.file];
if (model) {
model.setValue(file.code);
}else {
let model = createModel(monaco, file);
models = {
...models,
[file.file]: model
};
}
});
enableTabSwitching(editor, models, tabContainer);
sidebar(editor, models, sidebarContainer);
};
const initializeEditor = async () => {
monaco = await initializeMonaco();
// const files = await fetchProjectFiles();
// reCreateEditor(files)
};
onMount(async () => {
await initializeEditor()
socket.on('code', async function (data) {
if(data.from === 'coder'){
reCreateEditor(data.files);
}else{
patchOrFeature(data.files)
}
});
projectFiles.subscribe((files) => {
if (files){
reCreateEditor(files);
}
});
});
onDestroy(() => {
disposeEditor(editor);
models = {};
});
// $: if ($selectedProject && $selectedProject != 'select project') {
// initializeEditor()
// }
</script>
<div
class="w-full h-full flex flex-1 flex-col border-[3px] overflow-hidden rounded-xl border-window-outline p-0"
>
<div class="flex items-center p-2 border-b bg-terminal-window-ribbon">
<div class="flex ml-2 mr-4 space-x-2">
<div class="w-3 h-3 rounded-full bg-terminal-window-dots"></div>
<div class="w-3 h-3 rounded-full bg-terminal-window-dots"></div>
<div class="w-3 h-3 rounded-full bg-terminal-window-dots"></div>
</div>
<div id="tabContainer" class="flex text-tertiary text-sm overflow-x-auto" bind:this={tabContainer} />
{#if Object.keys(models).length == 0}
<div class="flex items-center text-tertiary text-sm">Code viewer</div>
{/if}
</div>
<div class="h-full w-full flex">
<div class="min-w-[260px] overflow-y-auto bg-secondary h-full text-foreground text-sm flex flex-col pt-2" bind:this={sidebarContainer} />
<div class="h-full w-full rounded-bl-lg bg-terminal-window-background p-0" bind:this={editorContainer} />
</div>
</div>

View File

@@ -0,0 +1,104 @@
<script>
import { messages } from "$lib/store";
import { afterUpdate } from "svelte";
let messageContainer;
let previousMessageCount = 0;
afterUpdate(() => {
if ($messages && $messages.length > 0) {
messageContainer.scrollTo({
top: messageContainer.scrollHeight,
behavior: "smooth"
});
}
});
</script>
<div
id="message-container"
class="flex flex-col flex-1 gap-2 overflow-y-auto rounded-lg"
bind:this={messageContainer}
>
{#if $messages !== null}
<div class="flex flex-col divide-y-2">
{#each $messages as message}
<div class="flex items-start gap-2 px-2 py-4">
{#if message.from_devika}
<img
src="/assets/devika-avatar.png"
alt="Devika's Avatar"
class="flex-shrink-0 rounded-full avatar"
style="width: 28px; height: 28px;"
/>
{:else}
<img
src="/assets/user-avatar.svg"
alt="User's Avatar"
class="flex-shrink-0 rounded-full avatar"
style="width: 28px; height: 28px;"
/>
{/if}
<div class="flex flex-col w-full text-sm">
<p class="text-xs text-gray-400">
{message.from_devika ? "Devika" : "You"}
<span class="timestamp">{new Date(message.timestamp).toLocaleTimeString()}</span>
</p>
{#if message.from_devika && message.message.startsWith("{")}
<div class="flex flex-col w-full gap-5" contenteditable="false">
{@html `<strong>Here's my step-by-step plan:</strong>`}
<div class="flex flex-col gap-3">
{#if JSON.parse(message.message)}
{#each Object.entries(JSON.parse(message.message)) as [step, description]}
<div class="flex items-center gap-2">
<input type="checkbox" id="step-{step}" disabled />
<label for="step-{step}" class="cursor-auto"><strong>Step {step}</strong>: {description}</label>
</div>
{/each}
{/if}
</div>
</div>
{:else if /https?:\/\/[^\s]+/.test(message.message)}
<div class="w-full cursor-auto" contenteditable="false">
{@html message.message.replace(
/(https?:\/\/[^\s]+)/g,
'<u><a href="$1" target="_blank" style="font-weight: bold;">$1</a></u>'
)}
</div>
{:else}
<div
class="w-full"
contenteditable="false"
bind:innerHTML={message.message}
></div>
{/if}
</div>
</div>
{/each}
</div>
{/if}
</div>
<style>
.timestamp {
margin-left: 8px;
font-size: smaller;
color: #aaa;
}
#message-container {
scrollbar-width: none;
}
input[type="checkbox"] {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
-ms-appearance: none;
-o-appearance: none;
width: 12px;
height: 12px;
border: 2px solid black;
border-radius: 4px;
}
</style>

View File

@@ -0,0 +1,138 @@
<script>
import DOMPurify from "dompurify";
import { emitMessage, socketListener } from "$lib/sockets";
import { agentState, messages, isSending } from "$lib/store";
import { calculateTokens } from "$lib/token";
import { onMount } from "svelte";
import { Icons } from "../icons";
let inference_time = 0;
agentState.subscribe((value) => {
if (value !== null && value.agent_is_active == false) {
isSending.set(false);
}
if (value == null) {
inference_time = 0;
}
});
let messageInput = "";
// Function to escape HTML
function escapeHTML(input) {
const map = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#039;",
};
return input.replace(/[&<>"']/g, function (m) {
return map[m];
});
}
async function handleSendMessage() {
const projectName = localStorage.getItem("selectedProject");
const selectedModel = localStorage.getItem("selectedModel");
const serachEngine = localStorage.getItem("selectedSearchEngine");
if (!projectName) {
alert("Please select a project first!");
return;
}
if (!selectedModel) {
alert("Please select a model first!");
return;
}
const sanitizedMessage = DOMPurify.sanitize(messageInput);
const escapedMessage = escapeHTML(sanitizedMessage);
if (messageInput.trim() !== "" && escapedMessage.trim() !== "" && isSending) {
$isSending = true;
emitMessage("user-message", {
message: escapedMessage,
base_model: selectedModel,
project_name: projectName,
search_engine: serachEngine,
});
messageInput = "";
}
}
onMount(() => {
socketListener("inference", function (data) {
if (data["type"] == "time") {
inference_time = data["elapsed_time"];
}
});
});
function setTokenSize(event) {
const prompt = event.target.value;
let tokens = calculateTokens(prompt);
document.querySelector(".token-count").textContent = `${tokens}`;
}
</script>
<div class="flex flex-col gap-2">
<div class="flex gap-4 justify-between">
<div class="px-1 rounded-md text-xs">
Agent status:
{#if $agentState !== null}
{#if $agentState.agent_is_active}
<span class="text-green-500">Active</span>
{:else}
<span class="text-orange-600">Inactive</span>
{/if}
{:else}
Deactive
{/if}
</div>
<!-- {#if $agentState !== null} -->
<div class="px-1 rounded-md text-xs">
Model Inference: <span class="text-orange-600">{inference_time} sec</span>
</div>
<!-- {/if} -->
</div>
<div class="expandable-input relative">
<textarea
id="message-input"
class="w-full p-4 font-medium focus:text-foreground rounded-xl outline-none h-28 pr-20 bg-secondary
{$isSending ? 'cursor-not-allowed' : ''}"
placeholder="Type your message..."
disabled={$isSending}
bind:value={messageInput}
on:input={setTokenSize}
on:keydown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSendMessage();
document.querySelector(".token-count").textContent = 0;
}
}}
></textarea>
<button
on:click={handleSendMessage}
disabled={$isSending}
class="absolute text-secondary bg-primary p-2 right-4 bottom-6 rounded-full
{$isSending ? 'cursor-not-allowed' : ''}"
>
{@html Icons.CornerDownLeft}
</button>
<p class="absolute text-tertiary p-2 right-4 top-2">
<span class="token-count">0</span>
</p>
</div>
</div>
<style>
.expandable-input textarea {
min-height: 60px;
max-height: 200px;
resize: none;
}
</style>

View File

@@ -0,0 +1,142 @@
import loader from "@monaco-editor/loader";
import { Icons } from "../icons";
function getFileLanguage(fileType) {
const fileTypeToLanguage = {
js: "javascript",
jsx: "javascript",
ts: "typescript",
tsx: "typescript",
html: "html",
css: "css",
py: "python",
java: "java",
rb: "ruby",
php: "php",
cpp: "c++",
c: "c",
swift: "swift",
kt: "kotlin",
json: "json",
xml: "xml",
sql: "sql",
sh: "shell",
};
const language = fileTypeToLanguage[fileType.toLowerCase()];
return language;
}
const getTheme = () => {
const theme = localStorage.getItem("mode-watcher-mode");
return theme === "light" ? "vs-light" : "vs-dark";
};
export async function initializeMonaco() {
const monacoEditor = await import("monaco-editor");
loader.config({ monaco: monacoEditor.default });
return loader.init();
}
export async function initializeEditorRef(monaco, container) {
const editor = monaco.editor.create(container, {
theme: getTheme(),
readOnly: false,
automaticLayout: true,
});
return editor;
}
export function createModel(monaco, file) {
const model = monaco.editor.createModel(
file.code,
getFileLanguage(file.file.split(".").pop())
);
return model;
}
export function disposeEditor(editor) {
if(editor) editor.dispose();
}
export function enableTabSwitching(editor, models, tabContainer) {
tabContainer.innerHTML = "";
Object.keys(models).forEach((filename, index) => {
const tabElement = document.createElement("div");
tabElement.textContent = filename.split("/").pop();
tabElement.className = "tab p-2 me-2 rounded-lg text-sm cursor-pointer hover:bg-secondary text-primary whitespace-nowrap";
tabElement.setAttribute("data-filename", filename);
tabElement.addEventListener("click", () =>
switchTab(editor, models, filename, tabElement)
);
if (index === Object.keys(models).length - 1) {
tabElement.classList.add("bg-secondary");
}
tabContainer.appendChild(tabElement);
});
}
function switchTab(editor, models, filename, tabElement) {
Object.entries(models).forEach(([file, model]) => {
if (file === filename) {
editor.setModel(model);
}
});
const allTabElements = tabElement?.parentElement?.children;
for (let i = 0; i < allTabElements?.length; i++) {
allTabElements[i].classList.remove("bg-secondary");
}
tabElement.classList.add("bg-secondary");
}
export function sidebar(editor, models, sidebarContainer) {
sidebarContainer.innerHTML = "";
const createSidebarElement = (filename, isFolder) => {
const sidebarElement = document.createElement("div");
sidebarElement.classList.add("mx-3", "p-1", "px-2", "cursor-pointer");
if (isFolder) {
sidebarElement.innerHTML = `<p class="flex items-center gap-2">${Icons.Folder}${" "}${filename}</p>`;
// TODO implement folder collapse/expand to the element sidebarElement
} else {
sidebarElement.innerHTML = `<p class="flex items-center gap-2">${Icons.File}${" "}${filename}</p>`;
}
return sidebarElement;
};
const changeTabColor = (index) => {
const allTabElements = document.querySelectorAll("#tabContainer")[0].children;
for (let i = 0; i < allTabElements?.length; i++) {
allTabElements[i].classList.remove("bg-secondary");
}
allTabElements[index].classList.add("bg-secondary");
}
const folders = {};
Object.entries(models).forEach(([filename, model], modelIndex) => {
const parts = filename.split('/');
let currentFolder = sidebarContainer;
parts.forEach((part, index) => {
if (index === parts.length - 1) {
const fileElement = createSidebarElement(part, false);
fileElement.addEventListener("click", () => {
editor.setModel(model);
changeTabColor(modelIndex);
});
currentFolder.appendChild(fileElement);
} else {
const folderName = part;
if (!folders[folderName]) {
const folderElement = createSidebarElement(part, true);
currentFolder.appendChild(folderElement);
folders[folderName] = folderElement;
currentFolder = folderElement;
} else {
currentFolder = folders[folderName];
}
}
});
});
}

View File

@@ -0,0 +1,36 @@
<script>
import SidebarButton from "./ui/SidebarButton.svelte";
import { page } from "$app/stores";
import {Icons} from "./../icons"
let navItems = [
{
icon: Icons.HOME,
tooltip: "Home",
route: "/",
},
{
icon: Icons.SETTINGS,
tooltip: "Settings",
route: "/settings",
},
{
icon: Icons.LOGS,
tooltip: "Logs",
route: "/logs",
},
];
</script>
<div
class="flex flex-col text-tertiary mx-2 my-4 gap-6 items-center bg-secondary rounded-xl p-6"
>
{#each navItems as { icon, tooltip, route }, i}
<SidebarButton
icon={icon}
href={route}
{tooltip}
isSelected={$page.url.pathname == route}
/>
{/each}
</div>

View File

@@ -0,0 +1,102 @@
<script>
import { onMount } from "svelte";
import { Terminal } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
import { agentState } from "$lib/store";
import "@xterm/xterm/css/xterm.css";
onMount(async () => {
const terminalBg = getComputedStyle(document.body).getPropertyValue(
"--terminal-window-background"
);
const terminalFg = getComputedStyle(document.body).getPropertyValue(
"--terminal-window-foreground"
);
const terminal = new Terminal({
disableStdin: true,
cursorBlink: true,
convertEol: true,
rows: 1,
theme: {
background: terminalBg,
foreground: terminalFg,
innerText: terminalFg,
cursor: terminalFg,
selectionForeground: terminalBg,
selectionBackground: terminalFg
},
});
const fitAddon = new FitAddon();
terminal.loadAddon(fitAddon);
terminal.open(document.getElementById("terminal-content"));
fitAddon.fit();
let previousState = {};
agentState.subscribe((state) => {
if (state && state.terminal_session) {
let command = state.terminal_session.command || 'echo "Waiting..."';
let output = state.terminal_session.output || "Waiting...";
let title = state.terminal_session.title || "Terminal";
// Check if the current state is different from the previous state
if (
command !== previousState.command ||
output !== previousState.output ||
title !== previousState.title
) {
// addCommandAndOutput(command, output, title);
if (title) {
document.getElementById("terminal-title").innerText = title;
}
terminal.reset();
terminal.write(`$ ${command}\r\n\r\n${output}\r\n`);
// Update the previous state
previousState = { command, output, title };
}
} else {
// Reset the terminal
terminal.reset();
}
fitAddon.fit();
});
});
</script>
<div
class="w-full h-full flex flex-col border-[3px] overflow-hidden rounded-xl border-window-outline"
>
<div class="flex items-center p-2 border-b bg-terminal-window-ribbon">
<div class="flex ml-2 mr-4 space-x-2">
<div class="w-3 h-3 rounded-full bg-terminal-window-dots"></div>
<div class="w-3 h-3 rounded-full bg-terminal-window-dots"></div>
<div class="w-3 h-3 rounded-full bg-terminal-window-dots"></div>
</div>
<span id="terminal-title" class="text-tertiary text-sm">Terminal</span>
</div>
<div
id="terminal-content"
class="w-full h-full rounded-bl-lg bg-terminal-window-background "
></div>
</div>
<style>
#terminal-content :global(.xterm) {
padding: 10px;
}
#terminal-content :global(.xterm-screen) {
width: 100% !important;
}
#terminal-content :global(.xterm-rows) {
width: 100% !important;
height: 100% !important;
overflow-x: scroll !important;
/* hide the scrollbar */
scrollbar-width: none;
}
</style>

View File

@@ -0,0 +1,9 @@
<script>
export let height = '20'; // default value
export let direction = 'horizontal'; // default value
</script>
<div
class="w-[1px] bg-secondary shrink-0"
style="{direction === 'horizontal' ? `height: ${height}px;` : `width: 90%; height: 1px;`}"
/>

View File

@@ -0,0 +1,43 @@
<script>
export let href = '';
export let tooltip = '';
export let isSelected = false;
export let icon;
</script>
<a {href}>
<div class="nav-button relative">
<button
class={`flex justify-center w-full hover:transition-colors ${
isSelected ? 'text-btn-active' : 'text-tertiary'
}`}
>
{@html icon}
</button>
<span class="tooltip">{tooltip}</span>
</div>
</a>
<style>
.tooltip {
font-size: 12px;
background-color: black;
color: white;
text-align: center;
border-radius: 100px;
padding: 5px 10px;
position: absolute;
z-index: 1;
opacity: 0;
top: 50%;
left: 90%;
margin: 0px 12px;
transform: translateY(-50%);
transition: opacity 0.3s;
}
.nav-button:hover .tooltip {
visibility: visible;
opacity: 1;
}
</style>

View File

@@ -0,0 +1,12 @@
import { Pane } from "paneforge";
import Handle from "./resizable-handle.svelte";
import PaneGroup from "./resizable-pane-group.svelte";
export {
PaneGroup,
Pane,
Handle,
//
PaneGroup as ResizablePaneGroup,
Pane as ResizablePane,
Handle as ResizableHandle,
};

View File

@@ -0,0 +1,22 @@
<script>
import * as ResizablePrimitive from "paneforge";
import { cn } from "$lib/utils.js";
export let withHandle = false;
export let el = undefined;
let className = undefined;
export { className as class };
</script>
<ResizablePrimitive.PaneResizer
bind:el
class={cn(
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[direction=vertical]:h-px data-[direction=vertical]:w-full data-[direction=vertical]:after:left-0 data-[direction=vertical]:after:h-1 data-[direction=vertical]:after:w-full data-[direction=vertical]:after:-translate-y-1/2 data-[direction=vertical]:after:translate-x-0 [&[data-direction=vertical]>div]:rotate-90",
className
)}
>
{#if withHandle}
<div class="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
<!-- <GripVertical class="h-2.5 w-2.5" /> -->
</div>
{/if}
</ResizablePrimitive.PaneResizer>

View File

@@ -0,0 +1,19 @@
<script>
import * as ResizablePrimitive from "paneforge";
import { cn } from "$lib/utils.js";
let className = undefined;
export let direction;
export let paneGroup = undefined;
export let el = undefined;
export { className as class };
</script>
<ResizablePrimitive.PaneGroup
bind:el
bind:paneGroup
{direction}
class={cn("flex h-full w-full data-[direction=vertical]:flex-col", className)}
{...$$restProps}
>
<slot />
</ResizablePrimitive.PaneGroup>

View File

@@ -0,0 +1,31 @@
import { Select as SelectPrimitive } from "bits-ui";
import Label from "./select-label.svelte";
import Item from "./select-item.svelte";
import Content from "./select-content.svelte";
import Trigger from "./select-trigger.svelte";
import Separator from "./select-separator.svelte";
const Root = SelectPrimitive.Root;
const Group = SelectPrimitive.Group;
const Input = SelectPrimitive.Input;
const Value = SelectPrimitive.Value;
export {
Root,
Group,
Input,
Label,
Item,
Value,
Content,
Trigger,
Separator,
//
Root as Select,
Group as SelectGroup,
Input as SelectInput,
Label as SelectLabel,
Item as SelectItem,
Value as SelectValue,
Content as SelectContent,
Trigger as SelectTrigger,
Separator as SelectSeparator,
};

View File

@@ -0,0 +1,34 @@
<script>
import { Select as SelectPrimitive } from "bits-ui";
import { scale } from "svelte/transition";
import { cn, flyAndScale } from "$lib/utils.js";
export let sideOffset = 4;
export let inTransition = flyAndScale;
export let inTransitionConfig = undefined;
export let outTransition = scale;
export let outTransitionConfig = {
start: 0.95,
opacity: 0,
duration: 50,
};
let className = undefined;
export { className as class };
</script>
<SelectPrimitive.Content
{inTransition}
{inTransitionConfig}
{outTransition}
{outTransitionConfig}
{sideOffset}
class={cn(
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-background text-popover-foreground shadow-md outline-none",
className
)}
{...$$restProps}
on:keydown
>
<div class="w-full p-1">
<slot />
</div>
</SelectPrimitive.Content>

View File

@@ -0,0 +1,36 @@
<script>
import { Select as SelectPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { Icons } from "../../../icons";
let className = undefined;
export let value;
export let label = undefined;
export let disabled = undefined;
export { className as class };
</script>
<SelectPrimitive.Item
{value}
{disabled}
{label}
class={cn(
"relative flex w-full select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50 cursor-pointer",
className
)}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerleave
on:pointermove
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator class="h-4 w-4">
{@html Icons.Check}
</SelectPrimitive.ItemIndicator>
</span>
<slot>
{label || value}
</slot>
</SelectPrimitive.Item>

View File

@@ -0,0 +1,13 @@
<script>
import { Select as SelectPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let className = undefined;
export { className as class };
</script>
<SelectPrimitive.Label
class={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...$$restProps}
>
<slot />
</SelectPrimitive.Label>

View File

@@ -0,0 +1,8 @@
<script>
import { Select as SelectPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let className = undefined;
export { className as class };
</script>
<SelectPrimitive.Separator class={cn("-mx-1 my-1 h-px bg-muted", className)} {...$$restProps} />

View File

@@ -0,0 +1,23 @@
<script>
import { Select as SelectPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { Icons } from "../../../icons";
let className = undefined;
export { className as class };
</script>
<SelectPrimitive.Trigger
class={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...$$restProps}
let:builder
on:click
on:keydown
>
<slot {builder} />
<div class="h-4 w-4 opacity-50">
{@html Icons.ChevronDown}
</div>
</SelectPrimitive.Trigger>

View File

@@ -0,0 +1 @@
export { default as Toaster } from "./sonner.svelte";

View File

@@ -0,0 +1,22 @@
<script>
import { Toaster as Sonner } from "svelte-sonner";
import { mode } from "mode-watcher";
</script>
<Sonner
richColors
expand={true}
theme={$mode}
duration={5000}
position="bottom-left"
class="toaster group"
toastOptions={{
classes: {
toast: "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton: "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...$$restProps}
/>

View File

@@ -0,0 +1,18 @@
import { Tabs as TabsPrimitive } from "bits-ui";
import Content from "./tabs-content.svelte";
import List from "./tabs-list.svelte";
import Trigger from "./tabs-trigger.svelte";
const Root = TabsPrimitive.Root;
export {
Root,
Content,
List,
Trigger,
//
Root as Tabs,
Content as TabsContent,
List as TabsList,
Trigger as TabsTrigger,
};

View File

@@ -0,0 +1,18 @@
<script>
import { Tabs as TabsPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let className = undefined;
export let value;
export { className as class };
</script>
<TabsPrimitive.Content
class={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ms-4",
className
)}
{value}
{...$$restProps}
>
<slot />
</TabsPrimitive.Content>

View File

@@ -0,0 +1,16 @@
<script>
import { Tabs as TabsPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let className = undefined;
export { className as class };
</script>
<TabsPrimitive.List
class={cn(
"inline-flex h-10 items-center justify-start rounded-md bg-muted p-1 text-muted-foreground mb-3",
className
)}
{...$$restProps}
>
<slot />
</TabsPrimitive.List>

View File

@@ -0,0 +1,19 @@
<script>
import { Tabs as TabsPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let className = undefined;
export let value;
export { className as class };
</script>
<TabsPrimitive.Trigger
class={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-lg px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-secondary data-[state=active]:text-primary data-[state=active]:shadow-sm",
className
)}
{value}
{...$$restProps}
on:click
>
<slot />
</TabsPrimitive.Trigger>

11
ui/src/lib/icons.js Normal file
View File

@@ -0,0 +1,11 @@
export const Icons = {
HOME: '<svg width="23" height="23" viewBox="0 0 23 23" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M12.8181 3.04591C12.4953 2.80375 12.1026 2.67285 11.699 2.67285C11.2954 2.67285 10.9027 2.80375 10.5798 3.04591L4.05135 7.94225C3.81969 8.11599 3.63166 8.34129 3.50216 8.60029C3.37266 8.85929 3.30524 9.14489 3.30524 9.43447V18.2945C3.30524 18.7892 3.50176 19.2637 3.85156 19.6135C4.20137 19.9633 4.67581 20.1598 5.17051 20.1598H8.80779C9.07988 20.1598 9.34082 20.0517 9.53321 19.8593C9.72561 19.6669 9.83369 19.406 9.83369 19.1339V14.564C9.83369 14.0693 10.0302 13.5948 10.38 13.245C10.7298 12.8952 11.2043 12.6987 11.699 12.6987C12.1937 12.6987 12.6681 12.8952 13.0179 13.245C13.3677 13.5948 13.5642 14.0693 13.5642 14.564V19.1339C13.5642 19.406 13.6723 19.6669 13.8647 19.8593C14.0571 20.0517 14.3181 20.1598 14.5901 20.1598H18.2274C18.7221 20.1598 19.1966 19.9633 19.5464 19.6135C19.8962 19.2637 20.0927 18.7892 20.0927 18.2945V9.43447C20.0927 9.14489 20.0253 8.85929 19.8958 8.60029C19.7663 8.34129 19.5782 8.11599 19.3466 7.94225L12.8181 3.04591Z" fill="currentColor" /> </svg>',
SETTINGS: '<svg width="23" height="23" viewBox="0 0 23 23" fill="none" xmlns="http://www.w3.org/2000/svg"> <path fill-rule="evenodd" clip-rule="evenodd" d="M13.8245 2.34879C13.4794 2.20703 13.0411 2.20703 12.1653 2.20703C11.2896 2.20703 10.8522 2.20703 10.5062 2.34879C10.0468 2.53738 9.68114 2.90069 9.4896 3.35884C9.40193 3.56682 9.36836 3.81023 9.35437 4.1637C9.34815 4.41981 9.27671 4.67009 9.14681 4.8909C9.0169 5.1117 8.83283 5.29572 8.61199 5.42556C8.38698 5.55063 8.13405 5.61689 7.87663 5.61819C7.6192 5.61949 7.36562 5.5558 7.13936 5.43302C6.82412 5.26701 6.59563 5.17561 6.36993 5.14577C5.87615 5.08094 5.37674 5.21366 4.9803 5.51509C4.68372 5.74079 4.46455 6.11758 4.02715 6.87021C3.58881 7.62285 3.37057 7.9987 3.32114 8.3671C3.25586 8.85766 3.39016 9.35383 3.6942 9.74646C3.83223 9.92553 4.02715 10.0757 4.32839 10.2641C4.77326 10.5411 5.05864 11.013 5.05864 11.5334C5.05864 12.0538 4.77326 12.5257 4.32932 12.8018C4.02715 12.9911 3.83223 13.1413 3.69326 13.3203C3.54319 13.5143 3.43303 13.736 3.36916 13.9728C3.30529 14.2095 3.28897 14.4566 3.32114 14.6997C3.37057 15.0672 3.58881 15.4439 4.02715 16.1966C4.46549 16.9492 4.68372 17.3251 4.9803 17.5517C5.37574 17.8529 5.87563 17.9854 6.36993 17.921C6.59563 17.8912 6.82412 17.7998 7.13936 17.6338C7.36573 17.5108 7.61949 17.447 7.8771 17.4483C8.1347 17.4497 8.3878 17.516 8.61292 17.6412C9.06618 17.9024 9.33478 18.3827 9.35437 18.9031C9.36836 19.2575 9.40193 19.5 9.4896 19.708C9.67986 20.1649 10.0455 20.5287 10.5062 20.718C10.8512 20.8598 11.2896 20.8598 12.1653 20.8598C13.0411 20.8598 13.4794 20.8598 13.8245 20.718C14.2839 20.5294 14.6495 20.1661 14.8411 19.708C14.9287 19.5 14.9623 19.2575 14.9763 18.9031C14.995 18.3827 15.2645 17.9014 15.7187 17.6412C15.9437 17.5162 16.1966 17.4499 16.454 17.4486C16.7115 17.4473 16.9651 17.511 17.1913 17.6338C17.5065 17.7998 17.735 17.8912 17.9607 17.921C18.455 17.9863 18.9549 17.8529 19.3504 17.5517C19.6469 17.326 19.8661 16.9492 20.3035 16.1966C20.7419 15.4439 20.9601 15.0681 21.0095 14.6997C21.0416 14.4565 21.0251 14.2094 20.9611 13.9727C20.897 13.7359 20.7867 13.5142 20.6365 13.3203C20.4984 13.1413 20.3035 12.9911 20.0023 12.8027C19.5574 12.5257 19.272 12.0538 19.272 11.5334C19.272 11.013 19.5574 10.5411 20.0013 10.265C20.3035 10.0757 20.4984 9.92553 20.6374 9.74646C20.7875 9.55253 20.8976 9.33078 20.9615 9.09402C21.0254 8.85726 21.0417 8.6102 21.0095 8.3671C20.9601 7.99964 20.7419 7.62285 20.3035 6.87021C19.8652 6.11758 19.6469 5.74172 19.3504 5.51509C18.9539 5.21366 18.4545 5.08094 17.9607 5.14577C17.735 5.17561 17.5065 5.26701 17.1913 5.43302C16.9649 5.55597 16.7112 5.61975 16.4536 5.61845C16.196 5.61714 15.9429 5.55079 15.7177 5.42556C15.4971 5.29561 15.3132 5.11154 15.1835 4.89074C15.0537 4.66995 14.9824 4.41972 14.9763 4.1637C14.9623 3.8093 14.9287 3.56682 14.8411 3.35884C14.7463 3.13196 14.6077 2.92598 14.4333 2.75268C14.2588 2.57937 14.052 2.44213 13.8245 2.34879ZM12.1653 14.3313C13.7228 14.3313 14.9847 13.0788 14.9847 11.5334C14.9847 9.98802 13.7219 8.73549 12.1653 8.73549C10.6078 8.73549 9.34597 9.98802 9.34597 11.5334C9.34597 13.0788 10.6088 14.3313 12.1653 14.3313Z" fill="currentColor"/> </svg>',
LOGS: '<svg width="23" height="23" viewBox="0 0 23 23" fill="none" xmlns="http://www.w3.org/2000/svg"> <mask id="mask0_6_459" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="2" y="1" width="20" height="20"> <path d="M21.0261 11.3C21.0261 16.4509 16.8507 20.6264 11.6998 20.6264C6.54883 20.6264 2.37341 16.4509 2.37341 11.3C2.37341 6.14905 6.54883 1.97363 11.6998 1.97363C16.8507 1.97363 21.0261 6.14905 21.0261 11.3Z" fill="white" /> <path fill-rule="evenodd" clip-rule="evenodd" d="M11.6993 6.87012C11.8848 6.87012 12.0627 6.94381 12.1939 7.07499C12.3251 7.20617 12.3988 7.38408 12.3988 7.56959V11.011L14.5252 13.1374C14.5939 13.2015 14.649 13.2787 14.6873 13.3645C14.7255 13.4503 14.746 13.5429 14.7477 13.6368C14.7494 13.7308 14.7321 13.8241 14.6969 13.9111C14.6617 13.9982 14.6094 14.0774 14.5429 14.1438C14.4765 14.2102 14.3974 14.2626 14.3103 14.2977C14.2232 14.3329 14.1299 14.3502 14.036 14.3485C13.9421 14.3469 13.8495 14.3263 13.7636 14.2881C13.6778 14.2499 13.6006 14.1948 13.5366 14.126L11.205 11.7944C11.0738 11.6634 11 11.4856 10.9998 11.3001V7.56959C10.9998 7.38408 11.0735 7.20617 11.2047 7.07499C11.3359 6.94381 11.5138 6.87012 11.6993 6.87012Z" fill="black" /> </mask> <g mask="url(#mask0_6_459)"> <path d="M0.507568 0.10791H22.8908V22.4912H0.507568V0.10791Z" fill="currentColor" /> </g> </svg>',
X: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>',
ChevronDown : '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down"><path d="m6 9 6 6 6-6"/></svg>',
Check : '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check"><path d="M20 6 9 17l-5-5"/></svg>',
CornerDownLeft : '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-corner-down-left"><polyline points="9 10 4 15 9 20"/><path d="M20 4v7a4 4 0 0 1-4 4H4"/></svg>',
Folder: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-folder"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg>',
File: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-file"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/></svg>',
};

92
ui/src/lib/sockets.js Normal file
View File

@@ -0,0 +1,92 @@
import { socket } from "./api";
import { messages, agentState, isSending, tokenUsage } from "./store";
import { toast } from "svelte-sonner";
import { get } from "svelte/store";
let prevMonologue = null;
export function initializeSockets() {
socket.connect();
let state = get(agentState);
prevMonologue = state?.internal_monologue;
socket.emit("socket_connect", { data: "frontend connected!" });
socket.on("socket_response", function (msg) {
console.log(msg);
});
socket.on("server-message", function (data) {
console.log(data)
messages.update((msgs) => [...msgs, data["messages"]]);
});
socket.on("agent-state", function (state) {
const lastState = state[state.length - 1];
agentState.set(lastState);
if (lastState.completed) {
isSending.set(false);
}
});
socket.on("tokens", function (tokens) {
tokenUsage.set(tokens["token_usage"]);
});
socket.on("inference", function (error) {
if (error["type"] == "error") {
toast.error(error["message"]);
isSending.set(false);
} else if (error["type"] == "warning") {
toast.warning(error["message"]);
}
});
socket.on("info", function (info) {
if (info["type"] == "error") {
toast.error(info["message"]);
isSending.set(false);
} else if (info["type"] == "warning") {
toast.warning(info["message"]);
} else if (info["type"] == "info") {
toast.info(info["message"]);
}
});
agentState.subscribe((state) => {
function handleMonologueChange(newValue) {
if (newValue) {
toast(newValue);
}
}
if (
state &&
state.internal_monologue &&
state.internal_monologue !== prevMonologue
) {
handleMonologueChange(state.internal_monologue);
prevMonologue = state.internal_monologue;
}
});
}
export function destroySockets() {
if (socket.connected) {
socket.off("socket_response");
socket.off("server-message");
socket.off("agent-state");
socket.off("tokens");
socket.off("inference");
socket.off("info");
}
}
export function emitMessage(channel, message) {
socket.emit(channel, message);
}
export function socketListener(channel, callback) {
socket.on(channel, callback);
}

48
ui/src/lib/store.js Normal file
View File

@@ -0,0 +1,48 @@
import { writable } from 'svelte/store';
// Helper function to get item from localStorage
function getItemFromLocalStorage(key, defaultValue) {
const storedValue = localStorage.getItem(key);
if (storedValue) {
return storedValue;
}
localStorage.setItem(key, defaultValue);
return defaultValue;
}
// Helper function to handle subscription and local storage setting
function subscribeAndStore(store, key, defaultValue) {
store.set(getItemFromLocalStorage(key, defaultValue));
store.subscribe(value => {
localStorage.setItem(key, value);
});
}
// Server related stores
export const serverStatus = writable(false);
export const internet = writable(true);
// Message related stores
export const messages = writable([]);
export const projectFiles = writable(null);
// Selection related stores
export const selectedProject = writable('');
export const selectedModel = writable('');
export const selectedSearchEngine = writable('');
subscribeAndStore(selectedProject, 'selectedProject', 'select project');
subscribeAndStore(selectedModel, 'selectedModel', 'select model');
subscribeAndStore(selectedSearchEngine, 'selectedSearchEngine', 'select search engine');
// List related stores
export const projectList = writable([]);
export const modelList = writable({});
export const searchEngineList = writable([]);
// Agent related stores
export const agentState = writable(null);
export const isSending = writable(false);
// Token usage store
export const tokenUsage = writable(0);

13
ui/src/lib/token.js Normal file
View File

@@ -0,0 +1,13 @@
import { Tiktoken } from "tiktoken/lite";
import cl100k_base from "tiktoken/encoders/cl100k_base.json";
const encoding = new Tiktoken(
cl100k_base.bpe_ranks,
cl100k_base.special_tokens,
cl100k_base.pat_str
);
export function calculateTokens(text) {
const tokens = encoding.encode(text);
return tokens.length;
}

48
ui/src/lib/utils.js Normal file
View File

@@ -0,0 +1,48 @@
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { cubicOut } from "svelte/easing";
export function cn(...inputs) {
return twMerge(clsx(inputs));
}
export const flyAndScale = (
node,
params = { y: -8, x: 0, start: 0.95, duration: 150 }
) => {
const style = getComputedStyle(node);
const transform = style.transform === "none" ? "" : style.transform;
const scaleConversion = (valueA, scaleA, scaleB) => {
const [minA, maxA] = scaleA;
const [minB, maxB] = scaleB;
const percentage = (valueA - minA) / (maxA - minA);
const valueB = percentage * (maxB - minB) + minB;
return valueB;
};
const styleToString = (style) => {
return Object.keys(style).reduce((str, key) => {
if (style[key] === undefined) return str;
return str + `${key}:${style[key]};`;
}, "");
};
return {
duration: params.duration ?? 200,
delay: 0,
css: (t) => {
const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]);
const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]);
const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]);
return styleToString({
transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`,
opacity: t
});
},
easing: cubicOut
};
};

1
ui/src/routes/+layout.js Normal file
View File

@@ -0,0 +1 @@
export const ssr = false

View File

@@ -0,0 +1,15 @@
<script>
import Sidebar from "$lib/components/Sidebar.svelte";
import { Toaster } from "$lib/components/ui/sonner";
import { ModeWatcher } from "mode-watcher";
import "../app.pcss";
</script>
<main>
<div class="h-dvh w-full flex">
<Toaster richColors/>
<ModeWatcher />
<Sidebar />
<slot />
</div>
</main>

View File

@@ -0,0 +1,57 @@
<script>
import { onDestroy, onMount } from "svelte";
import { toast } from "svelte-sonner";
import ControlPanel from "$lib/components/ControlPanel.svelte";
import MessageContainer from "$lib/components/MessageContainer.svelte";
import MessageInput from "$lib/components/MessageInput.svelte";
import BrowserWidget from "$lib/components/BrowserWidget.svelte";
import TerminalWidget from "$lib/components/TerminalWidget.svelte";
import EditorWidget from "../lib/components/EditorWidget.svelte";
import * as Resizable from "$lib/components/ui/resizable/index.js";
import { serverStatus } from "$lib/store";
import { initializeSockets, destroySockets } from "$lib/sockets";
import { checkInternetStatus, checkServerStatus } from "$lib/api";
let resizeEnabled =
localStorage.getItem("resize") &&
localStorage.getItem("resize") === "enable";
onMount(() => {
const load = async () => {
await checkInternetStatus();
if(!(await checkServerStatus())) {
toast.error("Failed to connect to server");
return;
}
serverStatus.set(true);
await initializeSockets();
};
load();
});
onDestroy(() => {
destroySockets();
});
</script>
<div class="flex h-full flex-col flex-1 gap-4 p-4 overflow-hidden">
<ControlPanel />
<div class="flex h-full overflow-x-scroll">
<div class="flex flex-1 min-w-[calc(100vw-120px)] h-full gap-2">
<div class="flex flex-col gap-2 w-full h-full pr-4">
<MessageContainer />
<MessageInput />
</div>
<div class="flex flex-col gap-4 h-full w-full p-2">
<BrowserWidget />
<TerminalWidget />
</div>
</div>
<div class="flex flex-col gap-2 min-w-[calc(100vw-120px)] h-full pr-4 p-2">
<EditorWidget />
</div>
</div>
</div>

View File

@@ -0,0 +1,64 @@
<script>
import { fetchLogs } from "$lib/api";
import { onMount } from "svelte";
let logs = [];
let socket_logs = [];
const logColors = {
'ERROR': 'text-red-500',
'EXCEPT': 'text-red-500',
'WARNING': 'text-yellow-500',
'INFO': 'text-blue-500',
'DEBUG': 'text-gray-500'
};
function getTextColor(log) {
for (const key in logColors) {
if (log.includes(key)) {
return logColors[key];
}
}
return '';
}
onMount(async () => {
const log = await fetchLogs();
const last_100_logs = log.split("\n").slice(-100).reverse().filter(log => log !== "");
[logs, socket_logs] = last_100_logs.reduce(([logs, socket_logs], log) => {
if (log.includes("SOCKET")) {
socket_logs.push(log);
} else {
logs.push(log);
}
return [logs, socket_logs];
}, [[], []]);
});
</script>
<div class="p-4 h-full gap-8 flex flex-col overflow-x-clip">
<h1 class="text-3xl">Logs</h1>
<div class="flex gap-4 overflow-y-auto">
<div class="flex flex-col gap-4 w-1/2">
<h1 class="text-2xl">Request logs</h1>
<div class="flex flex-col gap-2">
{#each logs as log}
<p class=" whitespace-normal break-words {getTextColor(log)}">
{@html log}
</p>
{/each}
</div>
</div>
<div class="flex flex-col gap-4 w-1/2">
<h1 class="text-2xl">Socket logs</h1>
<div class="flex flex-col gap-2">
{#each socket_logs as log}
<p class="{getTextColor(log)}">
{@html log}
</p>
{/each}
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,327 @@
<script>
import { updateSettings, fetchSettings } from "$lib/api";
import { onMount } from "svelte";
import * as Tabs from "$lib/components/ui/tabs";
import { setMode } from "mode-watcher";
import * as Select from "$lib/components/ui/select/index.js";
import Seperator from "../../lib/components/ui/Seperator.svelte";
import { toast } from "svelte-sonner";
let settings = {};
let editMode = false;
let original = {};
function getSelectedTheme() {
let theme = localStorage.getItem('mode-watcher-mode');
if (theme === "light") {
return { value: "light", label: "Light" };
} else if (theme === "dark") {
return { value: "dark", label: "Dark" };
} else if (theme === "system") {
return { value: "system", label: "System" };
} else {
return { value: "system", label: "System" };
}
}
function getSelectedResize() {
let resize = localStorage.getItem('resize');
if (resize === "enable") {
return { value: "enable", label: "Enable" };
} else {
return { value: "disable", label: "Disable" };
}
}
let selectedTheme = getSelectedTheme();
let selectedResize = getSelectedResize();
function setResize(value) {
localStorage.setItem('resize', value);
}
onMount(async () => {
settings = await fetchSettings();
// this is for correcting order of apis shown in the settings page
settings["API_KEYS"] = {
"BING": settings["API_KEYS"]["BING"],
"GOOGLE_SEARCH": settings["API_KEYS"]["GOOGLE_SEARCH"],
"GOOGLE_SEARCH_ENGINE_ID": settings["API_KEYS"]["GOOGLE_SEARCH_ENGINE_ID"],
"CLAUDE": settings["API_KEYS"]["CLAUDE"],
"OPENAI": settings["API_KEYS"]["OPENAI"],
"GEMINI": settings["API_KEYS"]["GEMINI"],
"MISTRAL": settings["API_KEYS"]["MISTRAL"],
"GROQ": settings["API_KEYS"]["GROQ"],
"NETLIFY": settings["API_KEYS"]["NETLIFY"]
};
// make a copy of the original settings
original = JSON.parse(JSON.stringify(settings));
});
const save = async () => {
let updated = {};
for (let key in settings) {
for (let subkey in settings[key]) {
if (settings[key][subkey] !== original[key][subkey]) {
if (!updated[key]) {
updated[key] = {};
}
updated[key][subkey] = settings[key][subkey];
}
}
}
await updateSettings(updated);
editMode = !editMode;
};
const edit = () => {
editMode = !editMode;
};
</script>
<div class="p-4 h-full w-full gap-8 flex flex-col overflow-y-auto">
<h1 class="text-3xl">Settings</h1>
<div class="flex flex-col w-full text-sm">
<Tabs.Root
value="apikeys"
class="w-full flex flex-col justify-start ms-2"
>
<Tabs.List class="ps-0">
<Tabs.Trigger value="apikeys">API Keys</Tabs.Trigger>
<Tabs.Trigger value="endpoints">API Endpoints</Tabs.Trigger>
<Tabs.Trigger value="config">Config</Tabs.Trigger>
<Tabs.Trigger value="appearance">Appearance</Tabs.Trigger>
</Tabs.List>
<Seperator direction="vertical"/>
<Tabs.Content value="apikeys" class="mt-4">
{#if settings["API_KEYS"]}
<div class="flex gap-4 w-full">
<div class="flex flex-col gap-4 w-full">
<div class="flex flex-col gap-4">
{#each Object.entries(settings["API_KEYS"]) as [key, value]}
<div class="flex gap-1 items-center">
<p class="w-48">{key.toLowerCase()}</p>
<input
type={editMode ? "text" : "password"}
value={settings["API_KEYS"][key]}
on:input={(e) => settings["API_KEYS"][key] = e.target.value}
name={key}
class="p-2 border-2 w-1/2 rounded-lg {editMode
? ''
: ' text-gray-500'}"
readonly={!editMode}
/>
</div>
{/each}
</div>
</div>
</div>
{/if}
<div class="flex gap-4 mt-5">
{#if !editMode}
<button
id="btn-edit"
class="p-2 border-2 rounded-lg flex gap-3 items-center hover:bg-secondary"
on:click={edit}
>
<i class="fas fa-edit"></i>
Edit
</button>
{:else}
<button
id="btn-save"
class="p-2 border-2 rounded-lg flex gap-3 items-center hover:bg-secondary"
on:click={save}
>
<i class="fas fa-save"></i>
Save
</button>
{/if}
</div>
</Tabs.Content>
<Tabs.Content value="endpoints" class="mt-4">
{#if settings["API_ENDPOINTS"]}
<div class="flex gap-4 w-full">
<div class="flex flex-col w-full gap-4">
{#each Object.entries(settings["API_ENDPOINTS"]) as [key, value]}
<div class="flex gap-3 items-center">
<p class="w-28">{key.toLowerCase()}</p>
<input
type="text"
value={settings["API_ENDPOINTS"][key]}
on:input={(e) => settings["API_ENDPOINTS"][key] = e.target.value}
name={key}
class="p-2 border-2 w-1/2 rounded-lg {editMode
? ''
: 'text-gray-500'}"
readonly={!editMode}
/>
</div>
{/each}
</div>
</div>
{/if}
<div class="flex gap-4 mt-5">
{#if !editMode}
<button
id="btn-edit"
class="p-2 border-2 rounded-lg flex gap-3 items-center hover:bg-secondary"
on:click={edit}
>
<i class="fas fa-edit"></i>
Edit
</button>
{:else}
<button
id="btn-save"
class="p-2 border-2 rounded-lg flex gap-3 items-center hover:bg-secondary"
on:click={save}
>
<i class="fas fa-save"></i>
Save
</button>
{/if}
</div>
</Tabs.Content>
<Tabs.Content value="config" class="mt-4">
{#if settings["TIMEOUT"]}
<div class="flex flex-col gap-8 w-full">
<div class="flex flex-col gap-4">
<div class="text-xl font-semibold">
Timouts
</div>
<div class="flex flex-col w-64 gap-4">
{#each Object.entries(settings["TIMEOUT"]) as [key, value]}
<div class="flex gap-3 items-center">
<p class="w-28">{key.toLowerCase()}</p>
<input
type="number"
bind:value={settings["TIMEOUT"][key]}
name={key}
placeholder="in seconds"
class="p-2 border-2 w-1/2 rounded-lg {editMode
? ''
: 'text-gray-500'}"
readonly={!editMode}
/>
</div>
{/each}
</div>
</div>
<div class="flex flex-col gap-4">
<div class="text-xl font-semibold">
Logging
</div>
<div class="flex flex-col w-64 gap-4">
{#each Object.entries(settings["LOGGING"]) as [key, value]}
<div class="flex gap-10 items-center">
<p class="w-28">{key.toLowerCase()}</p>
<Select.Root onSelectedChange={(v)=>{settings["LOGGING"][key] = v.value}}
disabled={!editMode}>
<Select.Trigger class="w-[180px]" >
<Select.Value placeholder={settings["LOGGING"][key]} />
</Select.Trigger>
<Select.Content>
<Select.Group>
<Select.Item value={"true"} label={"True"}>true</Select.Item>
<Select.Item value={"false"} label={"False"}>false</Select.Item>
</Select.Group>
</Select.Content>
<Select.Input name={key} />
</Select.Root>
</div>
{/each}
</div>
</div>
</div>
{/if}
<div class="flex gap-4 mt-5">
{#if !editMode}
<button
id="btn-edit"
class="p-2 border-2 rounded-lg flex gap-3 items-center hover:bg-secondary"
on:click={edit}
>
<i class="fas fa-edit"></i>
Edit
</button>
{:else}
<button
id="btn-save"
class="p-2 border-2 rounded-lg flex gap-3 items-center hover:bg-secondary"
on:click={save}
>
<i class="fas fa-save"></i>
Save
</button>
{/if}
</div>
</Tabs.Content>
<Tabs.Content value="appearance" class="mt-4 w-fit">
<div class="flex w-full justify-between items-center my-2 gap-8">
<div>
Select a theme
</div>
<div>
<Select.Root onSelectedChange={(v)=>{setMode(v.value)}}>
<Select.Trigger class="w-[180px]">
<Select.Value placeholder={selectedTheme.label} />
</Select.Trigger>
<Select.Content>
<Select.Group>
<Select.Item value={"light"} label={"Light"}>Light</Select.Item>
<Select.Item value={"dark"} label={"Dark"}>Dark</Select.Item>
<Select.Item value={"system"} label={"System"}>System</Select.Item>
</Select.Group>
</Select.Content>
<Select.Input name="favoriteFruit" />
</Select.Root>
</div>
</div>
<div class="flex w-full justify-between items-center my-2 gap-8">
<div>
Enable tab resize
</div>
<div>
<Select.Root onSelectedChange={(v)=>{setResize(v.value)}}>
<Select.Trigger class="w-[180px]">
<Select.Value placeholder={selectedResize.label}/>
</Select.Trigger>
<Select.Content>
<Select.Group>
<Select.Item value={"enable"} label={"Enable"}>Enable</Select.Item>
<Select.Item value={"disable"} label={"Disable"}>Disable</Select.Item>
</Select.Group>
</Select.Content>
<Select.Input name="favoriteFruit" />
</Select.Root>
</div>
</div>
<div class="flex w-full justify-between items-center my-2 gap-8">
<div>
Reset layout
</div>
<div>
<button
class="min-w-[180px] p-2 border-2 rounded-lg flex gap-3 items-center justify-between hover:bg-secondary"
on:click={() => {
toast.warning("Resetting layout");
localStorage.removeItem('paneforge:default');
}}
>
Reset
<i class="fas fa-undo"></i>
</button>
</div>
</div>
</Tabs.Content>
</Tabs.Root>
</div>
</div>

BIN
ui/static/assets/bootup.mp3 Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 911 KiB

View File

@@ -0,0 +1,6 @@
<svg width="27" height="27" viewBox="0 0 27 27" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.5" d="M7.67828 11.212C7.67828 9.67344 7.67828 8.90417 8.15704 8.42651C8.63361 7.94775 9.40288 7.94775 10.9425 7.94775H15.2948C16.8334 7.94775 17.6026 7.94775 18.0803 8.42651C18.559 8.90417 18.559 9.67344 18.559 11.212V15.5643C18.559 17.1028 18.559 17.8721 18.0803 18.3498C17.6026 18.8285 16.8334 18.8285 15.2948 18.8285H10.9425C9.40397 18.8285 8.6347 18.8285 8.15704 18.3498C7.67828 17.8732 7.67828 17.1039 7.67828 15.5643V11.212Z" stroke="#81878C" stroke-width="1.39896"/>
<path d="M13.585 11.2119L12.0302 13.3881H14.2063L12.6514 15.5642" stroke="#81878C" stroke-width="1.39896" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.41382 13.3882C4.41382 9.28507 4.41382 7.23296 5.68904 5.95882C6.96318 4.68359 9.01529 4.68359 13.1184 4.68359C17.2216 4.68359 19.2737 4.68359 20.5478 5.95882C21.823 7.23296 21.823 9.28507 21.823 13.3882C21.823 17.4913 21.823 19.5434 20.5478 20.8176C19.2737 22.0928 17.2216 22.0928 13.1184 22.0928C9.01529 22.0928 6.96318 22.0928 5.68904 20.8176C4.41382 19.5434 4.41382 17.4913 4.41382 13.3882Z" stroke="#81878C" stroke-width="1.39896"/>
<path opacity="0.5" d="M4.41449 13.3881H2.23834M23.9999 13.3881H21.8237M4.41449 10.1239H2.23834M23.9999 10.1239H21.8237M4.41449 16.6523H2.23834M23.9999 16.6523H21.8237M13.1191 22.0927V24.2688M13.1191 2.50732V4.68348M9.85487 22.0927V24.2688M9.85487 2.50732V4.68348M16.3833 22.0927V24.2688M16.3833 2.50732V4.68348" stroke="#81878C" stroke-width="1.39896" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

View File

@@ -0,0 +1,5 @@
<svg width="24" height="23" viewBox="0 0 24 23" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.1188 9.43961C12.1791 9.43961 13.8493 7.76939 13.8493 5.70906C13.8493 3.64874 12.1791 1.97852 10.1188 1.97852C8.05847 1.97852 6.38824 3.64874 6.38824 5.70906C6.38824 7.76939 8.05847 9.43961 10.1188 9.43961Z" stroke="#81878C" stroke-width="1.39896"/>
<path d="M17.58 16.4342C17.58 18.7518 17.58 20.631 10.1189 20.631C2.65784 20.631 2.65784 18.7518 2.65784 16.4342C2.65784 14.1166 5.99854 12.2373 10.1189 12.2373C14.2393 12.2373 17.58 14.1166 17.58 16.4342Z" stroke="#81878C" stroke-width="1.39896"/>
<path d="M18.5125 1.97852C18.5125 1.97852 20.3778 3.09768 20.3778 5.70906C20.3778 8.32044 18.5125 9.43961 18.5125 9.43961M16.6472 3.84379C16.6472 3.84379 17.5799 4.40337 17.5799 5.70906C17.5799 7.01475 16.6472 7.57434 16.6472 7.57434" stroke="#81878C" stroke-width="1.39896" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 914 B

BIN
ui/static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 911 KiB

16
ui/svelte.config.js Normal file
View File

@@ -0,0 +1,16 @@
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
import adapter from "@sveltejs/adapter-auto";
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter(),
},
preprocess: [vitePreprocess({})],
};
export default config;

50
ui/tailwind.config.js Normal file
View File

@@ -0,0 +1,50 @@
/** @type {import('tailwindcss').Config} */
const config = {
darkMode: ["class"],
content: ["./src/**/*.{html,js,svelte,ts}"],
safelist: ["dark"],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px"
}
},
extend: {
colors: {
'primary': 'var(--primary)',
'background': 'var(--background)',
'secondary': 'var(--secondary)',
'tertiary': 'var(--tertiary)',
'foreground': 'var(--foreground)',
'foreground-invert': 'var(--foreground-invert)',
'foreground-light': 'var(--foreground-light)',
'foreground-secondary': 'var(--foreground-secondary)',
'border': 'var(--border)',
'btn-active': 'var(--btn-active)',
'seperator': 'var(--seperator)',
'window-outline': 'var(--window-outline)',
'browser-window-dots': 'var(--browser-window-dots)',
'browser-window-search': 'var(--browser-window-search)',
'browser-window-ribbon': 'var(--browser-window-ribbon)',
'browser-window-foreground': 'var(--browser-window-foreground)',
'browser-window-background': 'var(--browser-window-background)',
'terminal-window-dots': 'var(--terminal-window-dots)',
'terminal-window-ribbon': 'var(--terminal-window-ribbon)',
'terminal-window-background': 'var(--terminal-window-background)',
'terminal-window-foreground': 'var(--terminal-window-foreground)',
'slider-empty': 'var(--slider-empty)',
'slider-filled': 'var(--slider-filled)',
'slider-thumb': 'var(--slider-thumb)',
'monologue-background': 'var(--monologue-background)',
'monologue-outline': 'var(--monologue-outline)',
},
fontFamily: {
sans: ["Helvetica"]
}
}
},
};
export default config;

16
ui/vite.config.js Normal file
View File

@@ -0,0 +1,16 @@
import { sveltekit } from "@sveltejs/kit/vite";
import { defineConfig } from "vite";
import wasm from "vite-plugin-wasm";
export default defineConfig({
plugins: [sveltekit(), wasm()],
server: {
port: 3000,
},
preview: {
port: 3001,
},
build: {
target: "esnext",
},
});