diff --git a/frontend/src/api/bot_config.ts b/frontend/src/api/bot_config.ts new file mode 100644 index 00000000..1dff6c84 --- /dev/null +++ b/frontend/src/api/bot_config.ts @@ -0,0 +1,43 @@ +export interface BotConfig { + transport: string | null; + enabled: boolean | null; + homeserver: string | null; + username: string | null; + password: string | null; + room_ids: string[] | null; + slack_bot_token: string | null; + slack_signing_secret: string | null; + slack_channel_ids: string[] | null; +} + +const DEFAULT_API_BASE = "/api"; + +async function requestJson( + path: string, + options: RequestInit = {}, + baseUrl = DEFAULT_API_BASE, +): Promise { + const res = await fetch(`${baseUrl}${path}`, { + headers: { "Content-Type": "application/json", ...(options.headers ?? {}) }, + ...options, + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(text || `Request failed (${res.status})`); + } + return res.json() as Promise; +} + +export const botConfigApi = { + getConfig(baseUrl?: string): Promise { + return requestJson("/bot/config", {}, baseUrl); + }, + + saveConfig(config: BotConfig, baseUrl?: string): Promise { + return requestJson( + "/bot/config", + { method: "PUT", body: JSON.stringify(config) }, + baseUrl, + ); + }, +}; diff --git a/frontend/src/components/BotConfigPage.tsx b/frontend/src/components/BotConfigPage.tsx new file mode 100644 index 00000000..bdce0109 --- /dev/null +++ b/frontend/src/components/BotConfigPage.tsx @@ -0,0 +1,344 @@ +import * as React from "react"; +import type { BotConfig } from "../api/bot_config"; +import { botConfigApi } from "../api/bot_config"; + +const { useState, useEffect } = React; + +interface BotConfigPageProps { + onBack: () => void; +} + +const fieldStyle: React.CSSProperties = { + display: "flex", + flexDirection: "column", + gap: "4px", +}; + +const labelStyle: React.CSSProperties = { + fontSize: "0.8em", + color: "#aaa", + fontWeight: 500, +}; + +const inputStyle: React.CSSProperties = { + padding: "8px 10px", + borderRadius: "6px", + border: "1px solid #333", + background: "#1e1e1e", + color: "#ececec", + fontSize: "0.9em", + fontFamily: "monospace", + outline: "none", +}; + +const sectionStyle: React.CSSProperties = { + background: "#1e1e1e", + border: "1px solid #333", + borderRadius: "8px", + padding: "20px", + display: "flex", + flexDirection: "column", + gap: "14px", +}; + +const sectionTitleStyle: React.CSSProperties = { + fontSize: "0.85em", + fontWeight: 600, + color: "#aaa", + textTransform: "uppercase", + letterSpacing: "0.06em", + marginBottom: "2px", +}; + +function Field({ + label, + value, + onChange, + placeholder, + type = "text", +}: { + label: string; + value: string; + onChange: (v: string) => void; + placeholder?: string; + type?: string; +}) { + return ( +
+ + onChange(e.target.value)} + placeholder={placeholder} + style={inputStyle} + autoComplete="off" + /> +
+ ); +} + +function ListField({ + label, + value, + onChange, + placeholder, +}: { + label: string; + value: string[]; + onChange: (v: string[]) => void; + placeholder?: string; +}) { + return ( +
+ +