init devika repo
This commit is contained in:
12
ui/.gitignore
vendored
Normal file
12
ui/.gitignore
vendored
Normal 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
|
||||
BIN
ui/bun.lockb
Executable file
BIN
ui/bun.lockb
Executable file
Binary file not shown.
14
ui/components.json
Normal file
14
ui/components.json
Normal 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
40
ui/package.json
Normal 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
13
ui/postcss.config.cjs
Normal 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
15
ui/src/app.html
Normal 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
137
ui/src/app.pcss
Normal 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
165
ui/src/lib/api.js
Normal 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;
|
||||
}
|
||||
50
ui/src/lib/components/BrowserWidget.svelte
Normal file
50
ui/src/lib/components/BrowserWidget.svelte
Normal 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>
|
||||
300
ui/src/lib/components/ControlPanel.svelte
Normal file
300
ui/src/lib/components/ControlPanel.svelte
Normal 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>
|
||||
99
ui/src/lib/components/EditorWidget.svelte
Normal file
99
ui/src/lib/components/EditorWidget.svelte
Normal 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>
|
||||
104
ui/src/lib/components/MessageContainer.svelte
Normal file
104
ui/src/lib/components/MessageContainer.svelte
Normal 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>
|
||||
138
ui/src/lib/components/MessageInput.svelte
Normal file
138
ui/src/lib/components/MessageInput.svelte
Normal 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 = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
};
|
||||
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>
|
||||
142
ui/src/lib/components/MonacoEditor.js
Normal file
142
ui/src/lib/components/MonacoEditor.js
Normal 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];
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
36
ui/src/lib/components/Sidebar.svelte
Normal file
36
ui/src/lib/components/Sidebar.svelte
Normal 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>
|
||||
102
ui/src/lib/components/TerminalWidget.svelte
Normal file
102
ui/src/lib/components/TerminalWidget.svelte
Normal 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>
|
||||
9
ui/src/lib/components/ui/Seperator.svelte
Normal file
9
ui/src/lib/components/ui/Seperator.svelte
Normal 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;`}"
|
||||
/>
|
||||
43
ui/src/lib/components/ui/SidebarButton.svelte
Normal file
43
ui/src/lib/components/ui/SidebarButton.svelte
Normal 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>
|
||||
12
ui/src/lib/components/ui/resizable/index.js
Normal file
12
ui/src/lib/components/ui/resizable/index.js
Normal 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,
|
||||
};
|
||||
22
ui/src/lib/components/ui/resizable/resizable-handle.svelte
Normal file
22
ui/src/lib/components/ui/resizable/resizable-handle.svelte
Normal 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>
|
||||
@@ -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>
|
||||
31
ui/src/lib/components/ui/select/index.js
Normal file
31
ui/src/lib/components/ui/select/index.js
Normal 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,
|
||||
};
|
||||
34
ui/src/lib/components/ui/select/select-content.svelte
Normal file
34
ui/src/lib/components/ui/select/select-content.svelte
Normal 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>
|
||||
36
ui/src/lib/components/ui/select/select-item.svelte
Normal file
36
ui/src/lib/components/ui/select/select-item.svelte
Normal 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>
|
||||
13
ui/src/lib/components/ui/select/select-label.svelte
Normal file
13
ui/src/lib/components/ui/select/select-label.svelte
Normal 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>
|
||||
8
ui/src/lib/components/ui/select/select-separator.svelte
Normal file
8
ui/src/lib/components/ui/select/select-separator.svelte
Normal 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} />
|
||||
23
ui/src/lib/components/ui/select/select-trigger.svelte
Normal file
23
ui/src/lib/components/ui/select/select-trigger.svelte
Normal 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>
|
||||
1
ui/src/lib/components/ui/sonner/index.js
Normal file
1
ui/src/lib/components/ui/sonner/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default as Toaster } from "./sonner.svelte";
|
||||
22
ui/src/lib/components/ui/sonner/sonner.svelte
Normal file
22
ui/src/lib/components/ui/sonner/sonner.svelte
Normal 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}
|
||||
/>
|
||||
18
ui/src/lib/components/ui/tabs/index.js
Normal file
18
ui/src/lib/components/ui/tabs/index.js
Normal 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,
|
||||
};
|
||||
18
ui/src/lib/components/ui/tabs/tabs-content.svelte
Normal file
18
ui/src/lib/components/ui/tabs/tabs-content.svelte
Normal 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>
|
||||
16
ui/src/lib/components/ui/tabs/tabs-list.svelte
Normal file
16
ui/src/lib/components/ui/tabs/tabs-list.svelte
Normal 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>
|
||||
19
ui/src/lib/components/ui/tabs/tabs-trigger.svelte
Normal file
19
ui/src/lib/components/ui/tabs/tabs-trigger.svelte
Normal 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
11
ui/src/lib/icons.js
Normal 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
92
ui/src/lib/sockets.js
Normal 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
48
ui/src/lib/store.js
Normal 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
13
ui/src/lib/token.js
Normal 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
48
ui/src/lib/utils.js
Normal 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
1
ui/src/routes/+layout.js
Normal file
@@ -0,0 +1 @@
|
||||
export const ssr = false
|
||||
15
ui/src/routes/+layout.svelte
Normal file
15
ui/src/routes/+layout.svelte
Normal 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>
|
||||
57
ui/src/routes/+page.svelte
Normal file
57
ui/src/routes/+page.svelte
Normal 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>
|
||||
64
ui/src/routes/logs/+page.svelte
Normal file
64
ui/src/routes/logs/+page.svelte
Normal 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>
|
||||
327
ui/src/routes/settings/+page.svelte
Normal file
327
ui/src/routes/settings/+page.svelte
Normal 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
BIN
ui/static/assets/bootup.mp3
Normal file
Binary file not shown.
BIN
ui/static/assets/devika-avatar.png
Normal file
BIN
ui/static/assets/devika-avatar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 911 KiB |
6
ui/static/assets/devika-avatar.svg
Normal file
6
ui/static/assets/devika-avatar.svg
Normal 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 |
1
ui/static/assets/loading-lottie.json
Normal file
1
ui/static/assets/loading-lottie.json
Normal file
File diff suppressed because one or more lines are too long
BIN
ui/static/assets/user-avatar.png
Normal file
BIN
ui/static/assets/user-avatar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
5
ui/static/assets/user-avatar.svg
Normal file
5
ui/static/assets/user-avatar.svg
Normal 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
BIN
ui/static/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 911 KiB |
16
ui/svelte.config.js
Normal file
16
ui/svelte.config.js
Normal 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
50
ui/tailwind.config.js
Normal 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
16
ui/vite.config.js
Normal 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",
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user