feat: admin panelini ve kurulum dokumanlarini genislet

This commit is contained in:
2026-03-22 04:46:02 +03:00
parent 5f4c19a18d
commit 37da564a5f
14 changed files with 728 additions and 49 deletions

View File

@@ -2,21 +2,29 @@ import { FormEvent, useEffect, useState } from "react";
import { api } from "./api";
import type {
AutomationRecord,
DashboardSnapshot,
MemoryRecord,
OllamaStatus,
RuntimeSettings,
TelegramStatus,
UserProfileRecord,
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",
model_provider: "local",
local_base_url: "http://127.0.0.1:1234",
local_model: "qwen3-vl-8b-instruct-mlx@5bit",
zai_model: "glm-5",
anythingllm_base_url: "http://127.0.0.1:3001",
anythingllm_workspace_slug: "wiseclaw",
tools: [
{ name: "brave_search", enabled: true },
{ name: "second_brain", enabled: true },
{ name: "browser_use", enabled: true },
{ name: "searxng_search", enabled: false },
{ name: "web_fetch", enabled: true },
{ name: "apple_notes", enabled: true },
@@ -29,12 +37,25 @@ export function App() {
const [dashboard, setDashboard] = useState<DashboardSnapshot | null>(null);
const [settings, setSettings] = useState<RuntimeSettings>(defaultSettings);
const [users, setUsers] = useState<UserRecord[]>([]);
const [profiles, setProfiles] = useState<UserProfileRecord[]>([]);
const [automations, setAutomations] = useState<AutomationRecord[]>([]);
const [memory, setMemory] = useState<MemoryRecord[]>([]);
const [secretMask, setSecretMask] = useState("");
const [secretValue, setSecretValue] = useState("");
const [zaiSecretMask, setZaiSecretMask] = useState("");
const [zaiSecretValue, setZaiSecretValue] = useState("");
const [anythingSecretMask, setAnythingSecretMask] = useState("");
const [anythingSecretValue, setAnythingSecretValue] = useState("");
const [ollamaStatus, setOllamaStatus] = useState<OllamaStatus | null>(null);
const [telegramStatus, setTelegramStatus] = useState<TelegramStatus | null>(null);
const [status, setStatus] = useState("Loading WiseClaw admin...");
const providerLabel = settings.model_provider === "local" ? "Local (LM Studio)" : "Z.AI";
const searchProviderLabel = settings.search_provider === "brave" ? "Brave" : "SearXNG";
const llmStatusLabel = settings.model_provider === "local" ? "LM Studio status" : "Z.AI status";
const llmStatusHint =
settings.model_provider === "local"
? "Checking local model endpoint..."
: "Checking remote Z.AI endpoint...";
useEffect(() => {
void load();
@@ -42,21 +63,29 @@ export function App() {
async function load() {
try {
const [dashboardData, settingsData, userData, memoryData, secretData, ollamaData, telegramData] =
const [dashboardData, settingsData, userData, profileData, automationData, memoryData, secretData, zaiSecretData, anythingSecretData, ollamaData, telegramData] =
await Promise.all([
api.getDashboard(),
api.getSettings(),
api.getUsers(),
api.getProfiles(),
api.getAutomations(),
api.getMemory(),
api.getSecretMask("brave_api_key"),
api.getSecretMask("zai_api_key"),
api.getSecretMask("anythingllm_api_key"),
api.getOllamaStatus(),
api.getTelegramStatus(),
]);
setDashboard(dashboardData);
setSettings(settingsData);
setUsers(userData);
setProfiles(profileData);
setAutomations(automationData);
setMemory(memoryData);
setSecretMask(secretData.masked);
setZaiSecretMask(zaiSecretData.masked);
setAnythingSecretMask(anythingSecretData.masked);
setOllamaStatus(ollamaData);
setTelegramStatus(telegramData);
setStatus("WiseClaw admin ready.");
@@ -84,6 +113,28 @@ export function App() {
await load();
}
async function handleZaiSecretSubmit(event: FormEvent) {
event.preventDefault();
if (!zaiSecretValue.trim()) {
return;
}
await api.saveSecret("zai_api_key", zaiSecretValue.trim());
setZaiSecretValue("");
setStatus("Z.AI API key updated.");
await load();
}
async function handleAnythingSecretSubmit(event: FormEvent) {
event.preventDefault();
if (!anythingSecretValue.trim()) {
return;
}
await api.saveSecret("anythingllm_api_key", anythingSecretValue.trim());
setAnythingSecretValue("");
setStatus("AnythingLLM API key updated.");
await load();
}
async function handleAddUser(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const form = new FormData(event.currentTarget);
@@ -133,7 +184,7 @@ export function App() {
</div>
<div>
<span>Model</span>
<strong>{settings.default_model}</strong>
<strong>{settings.model_provider === "local" ? settings.local_model : settings.zai_model}</strong>
</div>
</div>
</aside>
@@ -151,21 +202,21 @@ export function App() {
</div>
<div>
<span>Search provider</span>
<strong>{settings.search_provider}</strong>
<strong>{searchProviderLabel}</strong>
</div>
<div>
<span>Ollama</span>
<strong>{settings.ollama_base_url}</strong>
<span>Provider</span>
<strong>{providerLabel}</strong>
</div>
</div>
<div className="integration-grid">
<div className="integration-card">
<span>Ollama status</span>
<span>{llmStatusLabel}:</span>
<strong>{ollamaStatus?.reachable ? "Reachable" : "Offline"}</strong>
<p>{ollamaStatus?.message || "Checking..."}</p>
<p>{ollamaStatus?.message || llmStatusHint}</p>
</div>
<div className="integration-card">
<span>Telegram status</span>
<span>Telegram status:</span>
<strong>{telegramStatus?.configured ? "Configured" : "Missing token"}</strong>
<p>{telegramStatus?.message || "Checking..."}</p>
</div>
@@ -196,6 +247,22 @@ export function App() {
</select>
</label>
<label>
Model provider
<select
value={settings.model_provider}
onChange={(event) =>
setSettings({
...settings,
model_provider: event.target.value as "local" | "zai",
})
}
>
<option value="local">Local (LM Studio)</option>
<option value="zai">Z.AI</option>
</select>
</label>
<label>
Search provider
<select
@@ -213,21 +280,59 @@ export function App() {
</label>
<label>
Ollama base URL
AnythingLLM base URL
<input
value={settings.ollama_base_url}
onChange={(event) => setSettings({ ...settings, ollama_base_url: event.target.value })}
value={settings.anythingllm_base_url}
onChange={(event) => setSettings({ ...settings, anythingllm_base_url: event.target.value })}
placeholder="http://127.0.0.1:3001"
/>
</label>
<label>
Default model
AnythingLLM workspace slug
<input
value={settings.default_model}
onChange={(event) => setSettings({ ...settings, default_model: event.target.value })}
value={settings.anythingllm_workspace_slug}
onChange={(event) => setSettings({ ...settings, anythingllm_workspace_slug: event.target.value })}
placeholder="wiseclaw"
/>
</label>
{settings.model_provider === "local" ? (
<>
<label>
LM Studio base URL
<input
value={settings.local_base_url}
onChange={(event) => setSettings({ ...settings, local_base_url: event.target.value })}
/>
</label>
<label>
Local model
<input
value={settings.local_model}
onChange={(event) => setSettings({ ...settings, local_model: event.target.value })}
/>
</label>
</>
) : (
<>
<p className="muted">Z.AI uses the fixed hosted API endpoint and the API key saved below.</p>
<label>
Z.AI model
<select
value={settings.zai_model}
onChange={(event) =>
setSettings({ ...settings, zai_model: event.target.value as "glm-4.7" | "glm-5" })
}
>
<option value="glm-4.7">glm-4.7</option>
<option value="glm-5">glm-5</option>
</select>
</label>
</>
)}
<div className="tool-list">
{settings.tools.map((tool) => (
<label key={tool.name} className="checkbox-row">
@@ -250,7 +355,7 @@ export function App() {
</form>
<div className="stack">
<form className="panel" onSubmit={handleSecretSubmit}>
<form className="panel secret-panel" onSubmit={handleSecretSubmit}>
<div className="panel-head">
<h3>Secrets</h3>
<button type="submit">Update</button>
@@ -267,6 +372,40 @@ export function App() {
</label>
</form>
<form className="panel secret-panel" onSubmit={handleZaiSecretSubmit}>
<div className="panel-head">
<h3>Z.AI Secret</h3>
<button type="submit">Update</button>
</div>
<p className="muted">Current Z.AI key: {zaiSecretMask || "not configured"}</p>
<label>
Z.AI API key
<input
type="password"
value={zaiSecretValue}
onChange={(event) => setZaiSecretValue(event.target.value)}
placeholder="Paste a new key"
/>
</label>
</form>
<form className="panel secret-panel" onSubmit={handleAnythingSecretSubmit}>
<div className="panel-head">
<h3>AnythingLLM Secret</h3>
<button type="submit">Update</button>
</div>
<p className="muted">Current AnythingLLM key: {anythingSecretMask || "not configured"}</p>
<label>
AnythingLLM API key
<input
type="password"
value={anythingSecretValue}
onChange={(event) => setAnythingSecretValue(event.target.value)}
placeholder="Paste a new key"
/>
</label>
</form>
<form className="panel" onSubmit={handleAddUser}>
<div className="panel-head">
<h3>Telegram Whitelist</h3>
@@ -297,6 +436,75 @@ export function App() {
</div>
</section>
<section className="grid two-up">
<div className="panel compact-fixed-panel">
<div className="panel-head">
<h3>User Profiles</h3>
</div>
<div className="list compact-scroll-list">
{profiles.length === 0 ? <span className="muted">No onboarding profiles yet.</span> : null}
{profiles.map((profile) => (
<div key={profile.telegram_user_id} className="list-row">
<strong>
{profile.display_name || `User ${profile.telegram_user_id}`} ·{" "}
{profile.onboarding_completed
? "Onboarding complete"
: `Step ${profile.last_onboarding_step + 1}/12`}
</strong>
<div>Telegram ID: {profile.telegram_user_id}</div>
<div>Ton: {profile.tone_preference || "belirtilmedi"}</div>
<div>Dil: {profile.language_preference || "belirtilmedi"}</div>
<div>Cevap uzunluğu: {profile.response_length || "belirtilmedi"}</div>
<div>Çalışma biçimi: {profile.workflow_preference || "belirtilmedi"}</div>
<div>
Kullanım amacı: {profile.primary_use_cases.length ? profile.primary_use_cases.join(", ") : "belirtilmedi"}
</div>
<div>
Öncelikler: {profile.answer_priorities.length ? profile.answer_priorities.join(", ") : "belirtilmedi"}
</div>
<div>
İlgi alanları: {profile.interests.length ? profile.interests.join(", ") : "belirtilmedi"}
</div>
<div>
Onay beklentileri:{" "}
{profile.approval_preferences.length ? profile.approval_preferences.join(", ") : "belirtilmedi"}
</div>
<div>Kaçınılacaklar: {profile.avoid_preferences || "belirtilmedi"}</div>
</div>
))}
</div>
</div>
<div className="panel compact-fixed-panel">
<div className="panel-head">
<h3>Automations</h3>
</div>
<div className="list compact-scroll-list">
{automations.length === 0 ? <span className="muted">No automations yet.</span> : null}
{automations.map((automation) => (
<div key={automation.id} className="list-row automation-row">
<strong>
#{automation.id} {automation.name} · {automation.status}
</strong>
<div>Telegram ID: {automation.telegram_user_id}</div>
<div>Prompt: {automation.prompt}</div>
<div>
Schedule:{" "}
{automation.schedule_type === "hourly"
? `every ${automation.interval_hours || 1} hour(s)`
: automation.schedule_type}
</div>
{automation.time_of_day ? <div>Time: {automation.time_of_day}</div> : null}
{automation.days_of_week.length ? <div>Days: {automation.days_of_week.join(", ")}</div> : null}
<div>Next run: {automation.next_run_at || "not scheduled"}</div>
<div>Last run: {automation.last_run_at || "never"}</div>
<div>Last result: {automation.last_result || "no result yet"}</div>
</div>
))}
</div>
</div>
</section>
<section className="grid two-up">
<div className="panel">
<div className="panel-head">
@@ -305,7 +513,7 @@ export function App() {
Clear
</button>
</div>
<div className="list">
<div className="list scroll-list">
{memory.length === 0 ? <span className="muted">No memory yet.</span> : null}
{memory.map((item, index) => (
<div key={`${item.id}-${index}`} className="list-row">
@@ -320,7 +528,7 @@ export function App() {
<div className="panel-head">
<h3>Recent Logs</h3>
</div>
<div className="list">
<div className="list scroll-list">
{(dashboard?.recent_logs || []).length === 0 ? (
<span className="muted">No recent logs.</span>
) : null}

View File

@@ -1,13 +1,15 @@
import type {
AutomationRecord,
DashboardSnapshot,
MemoryRecord,
OllamaStatus,
RuntimeSettings,
TelegramStatus,
UserProfileRecord,
UserRecord,
} from "./types";
const API_BASE = "http://127.0.0.1:8000";
const API_BASE = `${window.location.protocol}//${window.location.hostname}:8000`;
async function request<T>(path: string, init?: RequestInit): Promise<T> {
const response = await fetch(`${API_BASE}${path}`, {
@@ -33,6 +35,8 @@ export const api = {
body: JSON.stringify(payload),
}),
getUsers: () => request<UserRecord[]>("/admin/users"),
getProfiles: () => request<UserProfileRecord[]>("/admin/profiles"),
getAutomations: () => request<AutomationRecord[]>("/admin/automations"),
addUser: (payload: UserRecord) =>
request<UserRecord>("/admin/users", {
method: "POST",
@@ -49,6 +53,6 @@ export const api = {
method: "POST",
body: JSON.stringify({ key, value }),
}),
getOllamaStatus: () => request<OllamaStatus>("/admin/integrations/ollama"),
getOllamaStatus: () => request<OllamaStatus>("/admin/integrations/llm"),
getTelegramStatus: () => request<TelegramStatus>("/admin/integrations/telegram"),
};

View File

@@ -120,6 +120,7 @@ label {
padding: 2rem;
display: grid;
gap: 1.4rem;
min-width: 0;
}
.panel {
@@ -129,6 +130,22 @@ label {
padding: 1.2rem;
backdrop-filter: blur(10px);
box-shadow: 0 20px 60px rgba(72, 64, 39, 0.08);
min-width: 0;
overflow: hidden;
}
.fixed-log-panel {
display: grid;
grid-template-rows: auto minmax(0, 1fr);
height: calc(80 * 1.4em + 5.5rem);
align-self: start;
}
.compact-fixed-panel {
display: grid;
grid-template-rows: auto minmax(0, 1fr);
height: 600px;
align-self: start;
}
.hero {
@@ -161,6 +178,15 @@ label {
border: 1px solid rgba(31, 92, 102, 0.12);
}
.integration-card span,
.integration-card strong {
display: inline;
}
.integration-card strong {
margin-left: 0.3rem;
}
.integration-card p {
margin-bottom: 0;
color: #4f5b57;
@@ -170,11 +196,36 @@ label {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1.4rem;
min-width: 0;
align-items: start;
}
.stack {
display: grid;
gap: 1.4rem;
min-width: 0;
}
.secret-panel {
padding-top: 0.64rem;
padding-bottom: 0.64rem;
}
.secret-panel .panel-head {
margin-bottom: 0.24rem;
}
.secret-panel label {
gap: 0.2rem;
}
.secret-panel .muted {
margin-bottom: 0.1rem;
}
.secret-panel form,
.secret-panel {
gap: 0.36rem;
}
.panel-head {
@@ -190,6 +241,7 @@ label {
form {
display: grid;
gap: 0.9rem;
min-width: 0;
}
.checkbox-row {
@@ -216,11 +268,103 @@ form {
}
.list-row {
padding: 0.8rem 0.9rem;
padding: 0.65rem 0.75rem;
border-radius: 18px;
background: rgba(31, 36, 33, 0.05);
font-family: "IBM Plex Mono", "SF Mono", monospace;
font-size: 0.9rem;
font-size: 0.84rem;
line-height: 1.28;
min-width: 0;
max-width: 100%;
overflow-wrap: anywhere;
word-break: break-word;
white-space: pre-wrap;
}
.list {
min-width: 0;
}
.list .list-row strong {
display: block;
margin-bottom: 0.18rem;
}
.automation-row {
height: 250px;
overflow-y: auto;
align-content: start;
scrollbar-width: thin;
scrollbar-color: rgba(31, 92, 102, 0.72) rgba(233, 196, 106, 0.2);
}
.scroll-list {
height: calc(80 * 1.4em);
max-height: calc(80 * 1.4em);
overflow-y: auto;
overflow-x: hidden;
align-content: start;
padding-right: 0.35rem;
scrollbar-width: thin;
scrollbar-color: rgba(31, 92, 102, 0.72) rgba(233, 196, 106, 0.2);
}
.compact-scroll-list {
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding-right: 0.35rem;
scrollbar-width: thin;
scrollbar-color: rgba(31, 92, 102, 0.72) rgba(233, 196, 106, 0.2);
}
.scroll-list::-webkit-scrollbar {
width: 12px;
}
.compact-scroll-list::-webkit-scrollbar {
width: 12px;
}
.scroll-list::-webkit-scrollbar-track {
background: rgba(233, 196, 106, 0.18);
border-radius: 999px;
}
.compact-scroll-list::-webkit-scrollbar-track {
background: rgba(233, 196, 106, 0.18);
border-radius: 999px;
}
.scroll-list::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, rgba(31, 122, 140, 0.88), rgba(31, 92, 102, 0.72));
border-radius: 999px;
border: 2px solid rgba(255, 250, 242, 0.9);
}
.compact-scroll-list::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, rgba(31, 122, 140, 0.88), rgba(31, 92, 102, 0.72));
border-radius: 999px;
border: 2px solid rgba(255, 250, 242, 0.9);
}
.automation-row::-webkit-scrollbar {
width: 10px;
}
.automation-row::-webkit-scrollbar-track {
background: rgba(233, 196, 106, 0.18);
border-radius: 999px;
}
.automation-row::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, rgba(31, 122, 140, 0.88), rgba(31, 92, 102, 0.72));
border-radius: 999px;
border: 2px solid rgba(255, 250, 242, 0.9);
}
.scroll-list::-webkit-scrollbar-thumb:hover {
background: linear-gradient(180deg, rgba(31, 122, 140, 1), rgba(31, 92, 102, 0.88));
}
@media (max-width: 960px) {

View File

@@ -6,8 +6,12 @@ export type ToolToggle = {
export type RuntimeSettings = {
terminal_mode: 1 | 2 | 3;
search_provider: "brave" | "searxng";
ollama_base_url: string;
default_model: string;
model_provider: "local" | "zai";
local_base_url: string;
local_model: string;
zai_model: "glm-4.7" | "glm-5";
anythingllm_base_url: string;
anythingllm_workspace_slug: string;
tools: ToolToggle[];
};
@@ -25,6 +29,41 @@ export type UserRecord = {
is_active: boolean;
};
export type UserProfileRecord = {
telegram_user_id: number;
display_name?: string | null;
bio?: string | null;
occupation?: string | null;
primary_use_cases: string[];
answer_priorities: string[];
tone_preference?: string | null;
response_length?: string | null;
language_preference?: string | null;
workflow_preference?: string | null;
interests: string[];
approval_preferences: string[];
avoid_preferences?: string | null;
onboarding_completed: boolean;
last_onboarding_step: number;
};
export type AutomationRecord = {
id: number;
telegram_user_id: number;
name: string;
prompt: string;
schedule_type: "daily" | "weekdays" | "weekly" | "hourly";
interval_hours?: number | null;
time_of_day?: string | null;
days_of_week: string[];
status: "active" | "paused";
last_run_at?: string | null;
next_run_at?: string | null;
last_result?: string | null;
created_at: string;
updated_at: string;
};
export type MemoryRecord = {
id: number;
content: string;
@@ -34,6 +73,7 @@ export type MemoryRecord = {
export type OllamaStatus = {
reachable: boolean;
provider: "local" | "zai";
base_url: string;
model: string;
installed_models: string[];