feat: React tabanli yönetim panelini ekle
This commit is contained in:
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>WiseClaw Admin</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
1820
frontend/package-lock.json
generated
Normal file
1820
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
frontend/package.json
Normal file
23
frontend/package.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "wiseclaw-frontend",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^19.1.0",
|
||||||
|
"react-dom": "^19.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^19.1.2",
|
||||||
|
"@types/react-dom": "^19.1.2",
|
||||||
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
|
"vite": "^7.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
338
frontend/src/App.tsx
Normal file
338
frontend/src/App.tsx
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
import { FormEvent, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { api } from "./api";
|
||||||
|
import type {
|
||||||
|
DashboardSnapshot,
|
||||||
|
MemoryRecord,
|
||||||
|
OllamaStatus,
|
||||||
|
RuntimeSettings,
|
||||||
|
TelegramStatus,
|
||||||
|
UserRecord,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
const defaultSettings: RuntimeSettings = {
|
||||||
|
terminal_mode: 3,
|
||||||
|
search_provider: "brave",
|
||||||
|
ollama_base_url: "http://127.0.0.1:11434",
|
||||||
|
default_model: "qwen3.5:4b",
|
||||||
|
tools: [
|
||||||
|
{ name: "brave_search", enabled: true },
|
||||||
|
{ name: "searxng_search", enabled: false },
|
||||||
|
{ name: "web_fetch", enabled: true },
|
||||||
|
{ name: "apple_notes", enabled: true },
|
||||||
|
{ name: "files", enabled: true },
|
||||||
|
{ name: "terminal", enabled: true },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
const [dashboard, setDashboard] = useState<DashboardSnapshot | null>(null);
|
||||||
|
const [settings, setSettings] = useState<RuntimeSettings>(defaultSettings);
|
||||||
|
const [users, setUsers] = useState<UserRecord[]>([]);
|
||||||
|
const [memory, setMemory] = useState<MemoryRecord[]>([]);
|
||||||
|
const [secretMask, setSecretMask] = useState("");
|
||||||
|
const [secretValue, setSecretValue] = useState("");
|
||||||
|
const [ollamaStatus, setOllamaStatus] = useState<OllamaStatus | null>(null);
|
||||||
|
const [telegramStatus, setTelegramStatus] = useState<TelegramStatus | null>(null);
|
||||||
|
const [status, setStatus] = useState("Loading WiseClaw admin...");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void load();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
try {
|
||||||
|
const [dashboardData, settingsData, userData, memoryData, secretData, ollamaData, telegramData] =
|
||||||
|
await Promise.all([
|
||||||
|
api.getDashboard(),
|
||||||
|
api.getSettings(),
|
||||||
|
api.getUsers(),
|
||||||
|
api.getMemory(),
|
||||||
|
api.getSecretMask("brave_api_key"),
|
||||||
|
api.getOllamaStatus(),
|
||||||
|
api.getTelegramStatus(),
|
||||||
|
]);
|
||||||
|
setDashboard(dashboardData);
|
||||||
|
setSettings(settingsData);
|
||||||
|
setUsers(userData);
|
||||||
|
setMemory(memoryData);
|
||||||
|
setSecretMask(secretData.masked);
|
||||||
|
setOllamaStatus(ollamaData);
|
||||||
|
setTelegramStatus(telegramData);
|
||||||
|
setStatus("WiseClaw admin ready.");
|
||||||
|
} catch (error) {
|
||||||
|
setStatus(error instanceof Error ? error.message : "Failed to load admin data.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSettingsSubmit(event: FormEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
const saved = await api.saveSettings(settings);
|
||||||
|
setSettings(saved);
|
||||||
|
setStatus("Runtime settings saved.");
|
||||||
|
await load();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSecretSubmit(event: FormEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!secretValue.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await api.saveSecret("brave_api_key", secretValue.trim());
|
||||||
|
setSecretValue("");
|
||||||
|
setStatus("Brave API key updated.");
|
||||||
|
await load();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAddUser(event: FormEvent<HTMLFormElement>) {
|
||||||
|
event.preventDefault();
|
||||||
|
const form = new FormData(event.currentTarget);
|
||||||
|
const telegram_user_id = Number(form.get("telegram_user_id"));
|
||||||
|
if (!telegram_user_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await api.addUser({
|
||||||
|
telegram_user_id,
|
||||||
|
username: String(form.get("username") || "") || null,
|
||||||
|
display_name: String(form.get("display_name") || "") || null,
|
||||||
|
is_active: true,
|
||||||
|
});
|
||||||
|
event.currentTarget.reset();
|
||||||
|
setStatus("Whitelist user saved.");
|
||||||
|
await load();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleClearMemory() {
|
||||||
|
await api.clearMemory();
|
||||||
|
setStatus("Memory cleared.");
|
||||||
|
await load();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="shell">
|
||||||
|
<aside className="sidebar">
|
||||||
|
<div className="brand">
|
||||||
|
<span className="brand-mark">WC</span>
|
||||||
|
<div>
|
||||||
|
<h1>WiseClaw</h1>
|
||||||
|
<p>Local admin console</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="status-card">
|
||||||
|
<span>State</span>
|
||||||
|
<strong>{status}</strong>
|
||||||
|
</div>
|
||||||
|
<div className="summary-list">
|
||||||
|
<div>
|
||||||
|
<span>Whitelist</span>
|
||||||
|
<strong>{dashboard?.whitelist_count ?? 0}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Memory items</span>
|
||||||
|
<strong>{dashboard?.memory_items ?? 0}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Model</span>
|
||||||
|
<strong>{settings.default_model}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main className="content">
|
||||||
|
<section className="panel hero">
|
||||||
|
<div>
|
||||||
|
<p className="eyebrow">Admin Panel</p>
|
||||||
|
<h2>Policy, tools, and local secrets in one place.</h2>
|
||||||
|
</div>
|
||||||
|
<div className="hero-grid">
|
||||||
|
<div>
|
||||||
|
<span>Terminal mode</span>
|
||||||
|
<strong>{settings.terminal_mode}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Search provider</span>
|
||||||
|
<strong>{settings.search_provider}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Ollama</span>
|
||||||
|
<strong>{settings.ollama_base_url}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="integration-grid">
|
||||||
|
<div className="integration-card">
|
||||||
|
<span>Ollama status</span>
|
||||||
|
<strong>{ollamaStatus?.reachable ? "Reachable" : "Offline"}</strong>
|
||||||
|
<p>{ollamaStatus?.message || "Checking..."}</p>
|
||||||
|
</div>
|
||||||
|
<div className="integration-card">
|
||||||
|
<span>Telegram status</span>
|
||||||
|
<strong>{telegramStatus?.configured ? "Configured" : "Missing token"}</strong>
|
||||||
|
<p>{telegramStatus?.message || "Checking..."}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="grid two-up">
|
||||||
|
<form className="panel" onSubmit={handleSettingsSubmit}>
|
||||||
|
<div className="panel-head">
|
||||||
|
<h3>Runtime Settings</h3>
|
||||||
|
<button type="submit">Save</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Terminal mode
|
||||||
|
<select
|
||||||
|
value={settings.terminal_mode}
|
||||||
|
onChange={(event) =>
|
||||||
|
setSettings({
|
||||||
|
...settings,
|
||||||
|
terminal_mode: Number(event.target.value) as 1 | 2 | 3,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value={1}>1: Full auto</option>
|
||||||
|
<option value={2}>2: Always ask</option>
|
||||||
|
<option value={3}>3: Policy based</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Search provider
|
||||||
|
<select
|
||||||
|
value={settings.search_provider}
|
||||||
|
onChange={(event) =>
|
||||||
|
setSettings({
|
||||||
|
...settings,
|
||||||
|
search_provider: event.target.value as "brave" | "searxng",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="brave">Brave</option>
|
||||||
|
<option value="searxng">SearXNG</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Ollama base URL
|
||||||
|
<input
|
||||||
|
value={settings.ollama_base_url}
|
||||||
|
onChange={(event) => setSettings({ ...settings, ollama_base_url: event.target.value })}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Default model
|
||||||
|
<input
|
||||||
|
value={settings.default_model}
|
||||||
|
onChange={(event) => setSettings({ ...settings, default_model: event.target.value })}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className="tool-list">
|
||||||
|
{settings.tools.map((tool) => (
|
||||||
|
<label key={tool.name} className="checkbox-row">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={tool.enabled}
|
||||||
|
onChange={(event) =>
|
||||||
|
setSettings({
|
||||||
|
...settings,
|
||||||
|
tools: settings.tools.map((item) =>
|
||||||
|
item.name === tool.name ? { ...item, enabled: event.target.checked } : item,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span>{tool.name}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="stack">
|
||||||
|
<form className="panel" onSubmit={handleSecretSubmit}>
|
||||||
|
<div className="panel-head">
|
||||||
|
<h3>Secrets</h3>
|
||||||
|
<button type="submit">Update</button>
|
||||||
|
</div>
|
||||||
|
<p className="muted">Current Brave key: {secretMask || "not configured"}</p>
|
||||||
|
<label>
|
||||||
|
Brave API key
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={secretValue}
|
||||||
|
onChange={(event) => setSecretValue(event.target.value)}
|
||||||
|
placeholder="Paste a new key"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form className="panel" onSubmit={handleAddUser}>
|
||||||
|
<div className="panel-head">
|
||||||
|
<h3>Telegram Whitelist</h3>
|
||||||
|
<button type="submit">Add User</button>
|
||||||
|
</div>
|
||||||
|
<label>
|
||||||
|
Telegram user id
|
||||||
|
<input name="telegram_user_id" type="number" placeholder="123456789" />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Username
|
||||||
|
<input name="username" placeholder="@username" />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Display name
|
||||||
|
<input name="display_name" placeholder="Wise Operator" />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className="pill-list">
|
||||||
|
{users.length === 0 ? <span className="muted">No whitelist entries yet.</span> : null}
|
||||||
|
{users.map((user) => (
|
||||||
|
<span key={user.telegram_user_id} className="pill">
|
||||||
|
{user.display_name || user.username || user.telegram_user_id}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="grid two-up">
|
||||||
|
<div className="panel">
|
||||||
|
<div className="panel-head">
|
||||||
|
<h3>Memory</h3>
|
||||||
|
<button type="button" onClick={handleClearMemory}>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="list">
|
||||||
|
{memory.length === 0 ? <span className="muted">No memory yet.</span> : null}
|
||||||
|
{memory.map((item, index) => (
|
||||||
|
<div key={`${item.id}-${index}`} className="list-row">
|
||||||
|
<strong>{item.kind}</strong>
|
||||||
|
<div>{item.content}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="panel">
|
||||||
|
<div className="panel-head">
|
||||||
|
<h3>Recent Logs</h3>
|
||||||
|
</div>
|
||||||
|
<div className="list">
|
||||||
|
{(dashboard?.recent_logs || []).length === 0 ? (
|
||||||
|
<span className="muted">No recent logs.</span>
|
||||||
|
) : null}
|
||||||
|
{(dashboard?.recent_logs || []).map((item, index) => (
|
||||||
|
<div key={`${item}-${index}`} className="list-row">
|
||||||
|
{item}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
54
frontend/src/api.ts
Normal file
54
frontend/src/api.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import type {
|
||||||
|
DashboardSnapshot,
|
||||||
|
MemoryRecord,
|
||||||
|
OllamaStatus,
|
||||||
|
RuntimeSettings,
|
||||||
|
TelegramStatus,
|
||||||
|
UserRecord,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
const API_BASE = "http://127.0.0.1:8000";
|
||||||
|
|
||||||
|
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||||
|
const response = await fetch(`${API_BASE}${path}`, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
...init,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Request failed with ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json() as Promise<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const api = {
|
||||||
|
getDashboard: () => request<DashboardSnapshot>("/admin/dashboard"),
|
||||||
|
getSettings: () => request<RuntimeSettings>("/admin/settings"),
|
||||||
|
saveSettings: (payload: RuntimeSettings) =>
|
||||||
|
request<RuntimeSettings>("/admin/settings", {
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
}),
|
||||||
|
getUsers: () => request<UserRecord[]>("/admin/users"),
|
||||||
|
addUser: (payload: UserRecord) =>
|
||||||
|
request<UserRecord>("/admin/users", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
}),
|
||||||
|
getMemory: () => request<MemoryRecord[]>("/admin/memory"),
|
||||||
|
clearMemory: () =>
|
||||||
|
request<{ status: string }>("/admin/memory", {
|
||||||
|
method: "DELETE",
|
||||||
|
}),
|
||||||
|
getSecretMask: (key: string) => request<{ key: string; masked: string }>(`/admin/secrets/${key}`),
|
||||||
|
saveSecret: (key: string, value: string) =>
|
||||||
|
request<{ status: string }>("/admin/secrets", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ key, value }),
|
||||||
|
}),
|
||||||
|
getOllamaStatus: () => request<OllamaStatus>("/admin/integrations/ollama"),
|
||||||
|
getTelegramStatus: () => request<TelegramStatus>("/admin/integrations/telegram"),
|
||||||
|
};
|
||||||
12
frontend/src/main.tsx
Normal file
12
frontend/src/main.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
|
||||||
|
import { App } from "./App";
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
||||||
|
|
||||||
240
frontend/src/styles.css
Normal file
240
frontend/src/styles.css
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
|
font-family: "IBM Plex Sans", "Avenir Next", sans-serif;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top left, rgba(255, 209, 102, 0.35), transparent 30%),
|
||||||
|
radial-gradient(circle at top right, rgba(31, 122, 140, 0.24), transparent 32%),
|
||||||
|
linear-gradient(180deg, #f5f1e8 0%, #ebe3d2 100%);
|
||||||
|
color: #1f2421;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
select {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #1f5c66;
|
||||||
|
color: #f5f1e8;
|
||||||
|
padding: 0.72rem 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
select {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid rgba(31, 36, 33, 0.14);
|
||||||
|
border-radius: 16px;
|
||||||
|
background: rgba(255, 250, 242, 0.95);
|
||||||
|
padding: 0.85rem 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.45rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #36413d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 280px 1fr;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
padding: 2rem 1.2rem;
|
||||||
|
background: rgba(32, 41, 39, 0.92);
|
||||||
|
color: #f7f2e8;
|
||||||
|
display: grid;
|
||||||
|
align-content: start;
|
||||||
|
gap: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand h1,
|
||||||
|
.panel h3,
|
||||||
|
.hero h2 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand p,
|
||||||
|
.eyebrow,
|
||||||
|
.muted {
|
||||||
|
margin: 0;
|
||||||
|
color: rgba(247, 242, 232, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-mark {
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 18px;
|
||||||
|
background: linear-gradient(135deg, #f4a261, #e9c46a);
|
||||||
|
color: #18201e;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card,
|
||||||
|
.summary-list > div {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-list span,
|
||||||
|
.status-card span,
|
||||||
|
.hero-grid span {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 2rem;
|
||||||
|
display: grid;
|
||||||
|
gap: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
background: rgba(255, 250, 242, 0.9);
|
||||||
|
border: 1px solid rgba(31, 36, 33, 0.1);
|
||||||
|
border-radius: 28px;
|
||||||
|
padding: 1.2rem;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
box-shadow: 0 20px 60px rgba(72, 64, 39, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
display: grid;
|
||||||
|
gap: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-grid > div {
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: rgba(233, 196, 106, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-card {
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: rgba(31, 92, 102, 0.08);
|
||||||
|
border: 1px solid rgba(31, 92, 102, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-card p {
|
||||||
|
margin-bottom: 0;
|
||||||
|
color: #4f5b57;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid.two-up {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack {
|
||||||
|
display: grid;
|
||||||
|
gap: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-list,
|
||||||
|
.list,
|
||||||
|
form {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-row input {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill-list {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.6rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(31, 122, 140, 0.12);
|
||||||
|
color: #1f5c66;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-row {
|
||||||
|
padding: 0.8rem 0.9rem;
|
||||||
|
border-radius: 18px;
|
||||||
|
background: rgba(31, 36, 33, 0.05);
|
||||||
|
font-family: "IBM Plex Mono", "SF Mono", monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.shell,
|
||||||
|
.grid.two-up,
|
||||||
|
.hero-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
47
frontend/src/types.ts
Normal file
47
frontend/src/types.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
export type ToolToggle = {
|
||||||
|
name: string;
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RuntimeSettings = {
|
||||||
|
terminal_mode: 1 | 2 | 3;
|
||||||
|
search_provider: "brave" | "searxng";
|
||||||
|
ollama_base_url: string;
|
||||||
|
default_model: string;
|
||||||
|
tools: ToolToggle[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DashboardSnapshot = {
|
||||||
|
settings: RuntimeSettings;
|
||||||
|
whitelist_count: number;
|
||||||
|
memory_items: number;
|
||||||
|
recent_logs: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UserRecord = {
|
||||||
|
telegram_user_id: number;
|
||||||
|
username?: string | null;
|
||||||
|
display_name?: string | null;
|
||||||
|
is_active: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MemoryRecord = {
|
||||||
|
id: number;
|
||||||
|
content: string;
|
||||||
|
kind: string;
|
||||||
|
created_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OllamaStatus = {
|
||||||
|
reachable: boolean;
|
||||||
|
base_url: string;
|
||||||
|
model: string;
|
||||||
|
installed_models: string[];
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TelegramStatus = {
|
||||||
|
configured: boolean;
|
||||||
|
polling_active: boolean;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
22
frontend/tsconfig.json
Normal file
22
frontend/tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"allowJs": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
|
|
||||||
11
frontend/tsconfig.node.json
Normal file
11
frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
|
|
||||||
10
frontend/vite.config.ts
Normal file
10
frontend/vite.config.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
Reference in New Issue
Block a user