Oppo-TIA / frontend /src /TerminalView.tsx
DJ-Goanna-Coding's picture
Upload folder using huggingface_hub
67be617 verified
Raw History Blame Contribute Delete
8.25 kB
import { useEffect, useRef, useState } from "react";
type LineKind = "user" | "system" | "error" | "muted";
interface Line {
kind: LineKind;
text: string;
}
interface PersonaSummary {
name: string;
title: string;
temperature: number;
tools: string[];
}
interface Citation {
source: string;
index: number;
text: string;
score: number;
}
interface AskResponse {
persona: string;
query: string;
answer: string;
citations: Citation[];
model: string;
}
interface QueryResponse {
query: string;
results: Citation[];
}
const HELP = `T.I.A. terminal — available commands:
/personas List configured personas
/persona <name> Select active persona
/health Hit /v1/system/health
/search <query> Plain RAG search via /v1/harvest/query
/token <jwt> Set/replace the Bearer token (kept in memory only)
/clear Clear the screen
/help Show this message
<free text> Ask the active persona (defaults to Librarian)`;
export function TerminalView() {
const [lines, setLines] = useState<Line[]>([
{ kind: "system", text: "T.I.A. RAG Engine — Terminal v0.1" },
{ kind: "muted", text: "Type /help for available commands." },
]);
const [input, setInput] = useState("");
const [persona, setPersona] = useState("Librarian");
const [personas, setPersonas] = useState<PersonaSummary[]>([]);
const [token, setToken] = useState<string>("");
const [busy, setBusy] = useState(false);
const outputRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
outputRef.current?.scrollTo({ top: outputRef.current.scrollHeight });
}, [lines]);
useEffect(() => {
inputRef.current?.focus();
}, []);
function append(kind: LineKind, text: string) {
setLines((prev) => [...prev, { kind, text }]);
}
async function api(path: string, init?: RequestInit): Promise<Response> {
const headers = new Headers(init?.headers || {});
headers.set("Accept", "application/json");
if (init?.body && !headers.has("Content-Type")) {
headers.set("Content-Type", "application/json");
}
if (token) headers.set("Authorization", `Bearer ${token}`);
return fetch(path, { ...init, headers });
}
async function loadPersonas() {
try {
const res = await api("/v1/personas");
if (!res.ok) {
append("error", `GET /v1/personas → ${res.status}`);
return;
}
const data = (await res.json()) as { personas: PersonaSummary[] };
setPersonas(data.personas);
append(
"system",
`Personas: ${data.personas.map((p) => p.name).join(", ")}`
);
} catch (err) {
append("error", `Failed to load personas: ${(err as Error).message}`);
}
}
async function runHealth() {
try {
const res = await fetch("/v1/system/health");
const body = await res.json();
append("system", JSON.stringify(body, null, 2));
} catch (err) {
append("error", `health check failed: ${(err as Error).message}`);
}
}
async function runSearch(query: string) {
if (!query.trim()) {
append("error", "Usage: /search <query>");
return;
}
setBusy(true);
try {
const res = await api("/v1/harvest/query", {
method: "POST",
body: JSON.stringify({ query, top_k: 5 }),
});
if (!res.ok) {
append("error", `query failed: ${res.status} ${await res.text()}`);
return;
}
const data = (await res.json()) as QueryResponse;
if (!data.results.length) {
append("muted", "(no results)");
}
for (const hit of data.results) {
append(
"system",
`[${hit.source}#${hit.index}] (score=${hit.score})\n${hit.text}`
);
}
} catch (err) {
append("error", `search failed: ${(err as Error).message}`);
} finally {
setBusy(false);
}
}
async function askPersona(query: string) {
setBusy(true);
try {
const res = await api(`/v1/personas/${encodeURIComponent(persona)}/ask`, {
method: "POST",
body: JSON.stringify({ query, top_k: 5 }),
});
if (!res.ok) {
append(
"error",
`${persona} replied ${res.status}: ${await res.text()}`
);
return;
}
const data = (await res.json()) as AskResponse;
append("system", `${data.persona} (${data.model}):\n${data.answer}`);
if (data.citations.length) {
const cites = data.citations
.map((c) => ` • [${c.source}#${c.index}] (score=${c.score})`)
.join("\n");
append("muted", `citations:\n${cites}`);
}
} catch (err) {
append("error", `persona call failed: ${(err as Error).message}`);
} finally {
setBusy(false);
}
}
async function handleSubmit(raw: string) {
const command = raw.trim();
if (!command) return;
append("user", `❯ ${command}`);
if (command === "/help") {
append("muted", HELP);
return;
}
if (command === "/clear") {
setLines([]);
return;
}
if (command === "/personas") {
await loadPersonas();
return;
}
if (command === "/health") {
await runHealth();
return;
}
if (command.startsWith("/persona ")) {
const next = command.slice("/persona ".length).trim();
if (!next) {
append("error", "Usage: /persona <name>");
} else {
setPersona(next);
append("system", `Active persona set to: ${next}`);
}
return;
}
if (command.startsWith("/search ")) {
await runSearch(command.slice("/search ".length).trim());
return;
}
if (command.startsWith("/token ")) {
const next = command.slice("/token ".length).trim();
setToken(next);
append(
"system",
next ? "Bearer token updated (in-memory)." : "Bearer token cleared."
);
return;
}
if (command.startsWith("/")) {
append("error", `Unknown command: ${command}. Type /help.`);
return;
}
await askPersona(command);
}
function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
if (event.key === "Enter" && !busy) {
const value = input;
setInput("");
void handleSubmit(value);
}
}
return (
<div className="terminal">
<div className="terminal-header">
<div className="terminal-title">T.I.A. // TERMINAL</div>
<div className="terminal-controls">
<label htmlFor="persona-select">persona</label>
<select
id="persona-select"
value={persona}
onChange={(e) => setPersona(e.target.value)}
>
{(personas.length
? personas.map((p) => p.name)
: ["Librarian", "Vanguard", "Oracle"]
).map((name) => (
<option key={name} value={name}>
{name}
</option>
))}
</select>
<button onClick={() => void loadPersonas()} type="button">
refresh
</button>
<button onClick={() => void runHealth()} type="button">
health
</button>
</div>
</div>
<div className="terminal-output" ref={outputRef}>
{lines.map((line, i) => (
<div key={i} className={`terminal-line ${line.kind}`}>
{line.text}
</div>
))}
</div>
<div className="terminal-input">
<span>❯</span>
<input
ref={inputRef}
type="text"
autoComplete="off"
spellCheck={false}
disabled={busy}
value={input}
placeholder={busy ? "…" : `ask ${persona} or /help`}
onChange={(e) => setInput(e.target.value)}
onKeyDown={onKeyDown}
/>
</div>
</div>
);
}