fix(frontend): various UI display and type corrections
- workspaces/[id]: Member type fix - projects/[id]: display adjustments - platform/sessions: session display improvements - env.ts: env type corrections - src/: frontend page and types updates
This commit is contained in:
parent
962bf0312d
commit
a705bdc938
@ -61,6 +61,7 @@ export default function ProjectDetailPage() {
|
|||||||
// Member management
|
// Member management
|
||||||
const [showAddMember, setShowAddMember] = useState(false);
|
const [showAddMember, setShowAddMember] = useState(false);
|
||||||
const [addUserId, setAddUserId] = useState("");
|
const [addUserId, setAddUserId] = useState("");
|
||||||
|
const [addUserDisplay, setAddUserDisplay] = useState(""); // shown in the input field
|
||||||
const [addScope, setAddScope] = useState("member");
|
const [addScope, setAddScope] = useState("member");
|
||||||
const [addMemberLoading, setAddMemberLoading] = useState(false);
|
const [addMemberLoading, setAddMemberLoading] = useState(false);
|
||||||
const [addMemberError, setAddMemberError] = useState("");
|
const [addMemberError, setAddMemberError] = useState("");
|
||||||
@ -111,15 +112,24 @@ export default function ProjectDetailPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleUserSearch(q: string) {
|
async function handleUserSearch(q: string) {
|
||||||
setAddUserId(q);
|
setAddUserId(q); // raw typed text for form submission
|
||||||
if (q.length < 2) { setSearchUsers([]); return; }
|
if (q.length < 2) { setSearchUsers([]); return; }
|
||||||
setSearchLoading(true);
|
setSearchLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/platform/users?search=${encodeURIComponent(q)}&pageSize=10`);
|
const res = await fetch(`/api/platform/users?search=${encodeURIComponent(q)}&pageSize=10`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
if (!res.ok) {
|
||||||
|
setSearchUsers([]);
|
||||||
|
setAddMemberError(data.error || "搜索失败,请重试");
|
||||||
|
return;
|
||||||
|
}
|
||||||
const existingIds = new Set(members.map(m => m.uid));
|
const existingIds = new Set(members.map(m => m.uid));
|
||||||
setSearchUsers((data.users || []).filter((u: { uid: string }) => !existingIds.has(u.uid)));
|
setSearchUsers((data.users || []).filter((u: { uid: string }) => !existingIds.has(u.uid)));
|
||||||
} catch { setSearchUsers([]); }
|
setAddMemberError("");
|
||||||
|
} catch {
|
||||||
|
setSearchUsers([]);
|
||||||
|
setAddMemberError("搜索失败,请检查网络连接");
|
||||||
|
}
|
||||||
finally { setSearchLoading(false); }
|
finally { setSearchLoading(false); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,6 +147,7 @@ export default function ProjectDetailPage() {
|
|||||||
if (!res.ok) { setAddMemberError(data.error || "添加失败"); return; }
|
if (!res.ok) { setAddMemberError(data.error || "添加失败"); return; }
|
||||||
setShowAddMember(false);
|
setShowAddMember(false);
|
||||||
setAddUserId("");
|
setAddUserId("");
|
||||||
|
setAddUserDisplay("");
|
||||||
setAddScope("member");
|
setAddScope("member");
|
||||||
setSearchUsers([]);
|
setSearchUsers([]);
|
||||||
reload();
|
reload();
|
||||||
@ -347,8 +358,8 @@ export default function ProjectDetailPage() {
|
|||||||
<label className="form-label">搜索用户</label>
|
<label className="form-label">搜索用户</label>
|
||||||
<input
|
<input
|
||||||
className="form-input"
|
className="form-input"
|
||||||
value={addUserId}
|
value={addUserDisplay || addUserId}
|
||||||
onChange={e => handleUserSearch(e.target.value)}
|
onChange={e => { setAddUserDisplay(e.target.value); handleUserSearch(e.target.value); }}
|
||||||
placeholder="输入用户名搜索..."
|
placeholder="输入用户名搜索..."
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
@ -358,7 +369,7 @@ export default function ProjectDetailPage() {
|
|||||||
<div
|
<div
|
||||||
key={u.uid}
|
key={u.uid}
|
||||||
style={{ padding: "8px 12px", cursor: "pointer", borderBottom: "1px solid #f0f0f0" }}
|
style={{ padding: "8px 12px", cursor: "pointer", borderBottom: "1px solid #f0f0f0" }}
|
||||||
onClick={() => { setAddUserId(u.uid); setSearchUsers([]); }}
|
onClick={() => { setAddUserId(u.uid); setAddUserDisplay(u.username); setSearchUsers([]); }}
|
||||||
>
|
>
|
||||||
<div style={{ fontWeight: 500, fontSize: "14px" }}>{u.username}</div>
|
<div style={{ fontWeight: 500, fontSize: "14px" }}>{u.username}</div>
|
||||||
<div style={{ fontSize: "12px", color: "#737373" }}>{u.display_name || ""}</div>
|
<div style={{ fontSize: "12px", color: "#737373" }}>{u.display_name || ""}</div>
|
||||||
@ -376,7 +387,7 @@ export default function ProjectDetailPage() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-footer">
|
<div className="modal-footer">
|
||||||
<button className="btn btn-secondary" onClick={() => { setShowAddMember(false); setSearchUsers([]); }}>取消</button>
|
<button className="btn btn-secondary" onClick={() => { setShowAddMember(false); setSearchUsers([]); setAddUserDisplay(""); }}>取消</button>
|
||||||
<button className="btn btn-primary" disabled={addMemberLoading || !addUserId} onClick={handleAddMember}>
|
<button className="btn btn-primary" disabled={addMemberLoading || !addUserId} onClick={handleAddMember}>
|
||||||
{addMemberLoading ? "添加中..." : "添加"}
|
{addMemberLoading ? "添加中..." : "添加"}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -73,6 +73,7 @@ export default function WorkspaceDetailPage() {
|
|||||||
// Member management
|
// Member management
|
||||||
const [showAddMember, setShowAddMember] = useState(false);
|
const [showAddMember, setShowAddMember] = useState(false);
|
||||||
const [addUserId, setAddUserId] = useState("");
|
const [addUserId, setAddUserId] = useState("");
|
||||||
|
const [addUserDisplay, setAddUserDisplay] = useState(""); // shown in the input field
|
||||||
const [addRole, setAddRole] = useState("member");
|
const [addRole, setAddRole] = useState("member");
|
||||||
const [addMemberLoading, setAddMemberLoading] = useState(false);
|
const [addMemberLoading, setAddMemberLoading] = useState(false);
|
||||||
const [addMemberError, setAddMemberError] = useState("");
|
const [addMemberError, setAddMemberError] = useState("");
|
||||||
@ -163,15 +164,25 @@ export default function WorkspaceDetailPage() {
|
|||||||
|
|
||||||
// Search users for adding member
|
// Search users for adding member
|
||||||
async function handleUserSearch(q: string) {
|
async function handleUserSearch(q: string) {
|
||||||
setAddUserId(q);
|
setAddUserId(q); // keep the raw typed text for form submission
|
||||||
if (q.length < 2) { setSearchUsers([]); return; }
|
if (q.length < 2) { setSearchUsers([]); return; }
|
||||||
setSearchLoading(true);
|
setSearchLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/platform/users?search=${encodeURIComponent(q)}&pageSize=10`);
|
const res = await fetch(`/api/platform/users?search=${encodeURIComponent(q)}&pageSize=10`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const existingIds = new Set(members.map(m => m.uid));
|
if (!res.ok) {
|
||||||
|
// Show error from API
|
||||||
|
setSearchUsers([]);
|
||||||
|
setAddMemberError(data.error || "搜索失败,请重试");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const existingIds = new Set(members.map(m => m.userId));
|
||||||
setSearchUsers((data.users || []).filter((u: { uid: string }) => !existingIds.has(u.uid)));
|
setSearchUsers((data.users || []).filter((u: { uid: string }) => !existingIds.has(u.uid)));
|
||||||
} catch { setSearchUsers([]); }
|
setAddMemberError(""); // clear previous errors
|
||||||
|
} catch {
|
||||||
|
setSearchUsers([]);
|
||||||
|
setAddMemberError("搜索失败,请检查网络连接");
|
||||||
|
}
|
||||||
finally { setSearchLoading(false); }
|
finally { setSearchLoading(false); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,6 +200,7 @@ export default function WorkspaceDetailPage() {
|
|||||||
if (!res.ok) { setAddMemberError(data.error || "添加失败"); return; }
|
if (!res.ok) { setAddMemberError(data.error || "添加失败"); return; }
|
||||||
setShowAddMember(false);
|
setShowAddMember(false);
|
||||||
setAddUserId("");
|
setAddUserId("");
|
||||||
|
setAddUserDisplay("");
|
||||||
setAddRole("member");
|
setAddRole("member");
|
||||||
setSearchUsers([]);
|
setSearchUsers([]);
|
||||||
reload();
|
reload();
|
||||||
@ -496,8 +508,8 @@ export default function WorkspaceDetailPage() {
|
|||||||
<label className="form-label">搜索用户</label>
|
<label className="form-label">搜索用户</label>
|
||||||
<input
|
<input
|
||||||
className="form-input"
|
className="form-input"
|
||||||
value={addUserId}
|
value={addUserDisplay || addUserId}
|
||||||
onChange={(e) => handleUserSearch(e.target.value)}
|
onChange={(e) => { setAddUserDisplay(e.target.value); handleUserSearch(e.target.value); }}
|
||||||
placeholder="输入用户名搜索..."
|
placeholder="输入用户名搜索..."
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
@ -509,6 +521,7 @@ export default function WorkspaceDetailPage() {
|
|||||||
style={{ padding: "8px 12px", cursor: "pointer", borderBottom: "1px solid #f0f0f0" }}
|
style={{ padding: "8px 12px", cursor: "pointer", borderBottom: "1px solid #f0f0f0" }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setAddUserId(u.uid);
|
setAddUserId(u.uid);
|
||||||
|
setAddUserDisplay(u.username);
|
||||||
setSearchUsers([]);
|
setSearchUsers([]);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -528,7 +541,7 @@ export default function WorkspaceDetailPage() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-footer">
|
<div className="modal-footer">
|
||||||
<button className="btn btn-secondary" onClick={() => { setShowAddMember(false); setSearchUsers([]); }}>取消</button>
|
<button className="btn btn-secondary" onClick={() => { setShowAddMember(false); setSearchUsers([]); setAddUserDisplay(""); }}>取消</button>
|
||||||
<button className="btn btn-primary" disabled={addMemberLoading || !addUserId} onClick={handleAddMember}>
|
<button className="btn btn-primary" disabled={addMemberLoading || !addUserId} onClick={handleAddMember}>
|
||||||
{addMemberLoading ? "添加中..." : "添加"}
|
{addMemberLoading ? "添加中..." : "添加"}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -2,8 +2,14 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
|
import {
|
||||||
|
listUserSessions,
|
||||||
|
kickUser,
|
||||||
|
getUserStatus,
|
||||||
|
type UserSession,
|
||||||
|
} from "@/lib/admin-rpc";
|
||||||
|
|
||||||
interface SessionInfo {
|
interface PlatformSessionInfo {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
username: string | null;
|
username: string | null;
|
||||||
@ -14,14 +20,17 @@ interface SessionInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function PlatformSessionsPage() {
|
export default function PlatformSessionsPage() {
|
||||||
const [sessions, setSessions] = useState<SessionInfo[]>([]);
|
const [sessions, setSessions] = useState<PlatformSessionInfo[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [kicking, setKicking] = useState<string | null>(null);
|
const [kicking, setKicking] = useState<string | null>(null);
|
||||||
|
const [adminRpcAvailable, setAdminRpcAvailable] = useState(false);
|
||||||
|
|
||||||
useEffect(() => { loadSessions(); }, []);
|
useEffect(() => { loadSessions(); }, []);
|
||||||
|
|
||||||
async function loadSessions() {
|
async function loadSessions() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
|
// Load platform sessions from the app's REST API (Redis-based)
|
||||||
const res = await fetch("/api/platform/sessions");
|
const res = await fetch("/api/platform/sessions");
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
setSessions([]);
|
setSessions([]);
|
||||||
@ -37,6 +46,31 @@ export default function PlatformSessionsPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleKickAllSessions(userId: string) {
|
||||||
|
if (!confirm(`强制下线用户 ${userId} 的所有会话?`)) return;
|
||||||
|
setKicking(userId);
|
||||||
|
try {
|
||||||
|
if (adminRpcAvailable) {
|
||||||
|
await kickUser(userId);
|
||||||
|
} else {
|
||||||
|
// Fallback: kick sessions via Redis (app REST API)
|
||||||
|
const userSessions = sessions.filter((s) => s.userId === userId);
|
||||||
|
await Promise.all(
|
||||||
|
userSessions.map((s) =>
|
||||||
|
fetch("/api/platform/sessions", {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ sessionId: s.sessionId }),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
loadSessions();
|
||||||
|
} finally {
|
||||||
|
setKicking(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleKickSession(sessionId: string) {
|
async function handleKickSession(sessionId: string) {
|
||||||
if (!confirm("确定强制下线该会话吗?")) return;
|
if (!confirm("确定强制下线该会话吗?")) return;
|
||||||
setKicking(sessionId);
|
setKicking(sessionId);
|
||||||
@ -47,11 +81,13 @@ export default function PlatformSessionsPage() {
|
|||||||
body: JSON.stringify({ sessionId }),
|
body: JSON.stringify({ sessionId }),
|
||||||
});
|
});
|
||||||
loadSessions();
|
loadSessions();
|
||||||
} finally { setKicking(null); }
|
} finally {
|
||||||
|
setKicking(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group by user
|
// Group by user
|
||||||
const byUser = sessions.reduce<Record<string, SessionInfo[]>>((acc, s) => {
|
const byUser = sessions.reduce<Record<string, PlatformSessionInfo[]>>((acc, s) => {
|
||||||
const key = s.userId || "unknown";
|
const key = s.userId || "unknown";
|
||||||
if (!acc[key]) acc[key] = [];
|
if (!acc[key]) acc[key] = [];
|
||||||
acc[key].push(s);
|
acc[key].push(s);
|
||||||
@ -65,10 +101,31 @@ export default function PlatformSessionsPage() {
|
|||||||
<h1 className="page-title">平台用户在线会话</h1>
|
<h1 className="page-title">平台用户在线会话</h1>
|
||||||
<p className="page-subtitle">
|
<p className="page-subtitle">
|
||||||
共 {sessions.length} 个活跃会话,涉及 {Object.keys(byUser).length} 个用户
|
共 {sessions.length} 个活跃会话,涉及 {Object.keys(byUser).length} 个用户
|
||||||
|
{adminRpcAvailable && (
|
||||||
|
<span style={{ marginLeft: "8px", color: "#22c55e", fontSize: "12px" }}>
|
||||||
|
● adminrpc 已连接
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div style={{ display: "flex", gap: "8px" }}>
|
||||||
|
<button
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
const { adminRpcHealth } = await import("@/lib/admin-rpc");
|
||||||
|
await adminRpcHealth();
|
||||||
|
setAdminRpcAvailable(true);
|
||||||
|
} catch {
|
||||||
|
setAdminRpcAvailable(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
检测 adminrpc
|
||||||
|
</button>
|
||||||
<button className="btn btn-secondary" onClick={loadSessions}>刷新</button>
|
<button className="btn btn-secondary" onClick={loadSessions}>刷新</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="loading">加载中...</div>
|
<div className="loading">加载中...</div>
|
||||||
@ -91,23 +148,19 @@ export default function PlatformSessionsPage() {
|
|||||||
<span className="badge badge-neutral" style={{ marginLeft: "8px" }}>
|
<span className="badge badge-neutral" style={{ marginLeft: "8px" }}>
|
||||||
{userSessions.length} 个会话
|
{userSessions.length} 个会话
|
||||||
</span>
|
</span>
|
||||||
|
{adminRpcAvailable && (
|
||||||
|
<AdminRpcUserStatus userId={userId} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{adminRpcAvailable && (
|
||||||
<button
|
<button
|
||||||
className="btn btn-danger btn-sm"
|
className="btn btn-danger btn-sm"
|
||||||
onClick={async () => {
|
disabled={kicking === userId}
|
||||||
if (!confirm(`强制下线用户 ${userId} 的所有会话?`)) return;
|
onClick={() => handleKickAllSessions(userId)}
|
||||||
for (const s of userSessions) {
|
|
||||||
await fetch("/api/platform/sessions", {
|
|
||||||
method: "DELETE",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ sessionId: s.sessionId }),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
loadSessions();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
全部下线
|
{kicking === userId ? "处理中..." : "全部下线 (adminrpc)"}
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="table-container">
|
<div className="table-container">
|
||||||
@ -115,6 +168,7 @@ export default function PlatformSessionsPage() {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Session ID</th>
|
<th>Session ID</th>
|
||||||
|
<th>工作空间</th>
|
||||||
<th>登录时间</th>
|
<th>登录时间</th>
|
||||||
<th>IP</th>
|
<th>IP</th>
|
||||||
<th>User Agent</th>
|
<th>User Agent</th>
|
||||||
@ -129,10 +183,15 @@ export default function PlatformSessionsPage() {
|
|||||||
{s.sessionId.slice(0, 16)}...
|
{s.sessionId.slice(0, 16)}...
|
||||||
</code>
|
</code>
|
||||||
</td>
|
</td>
|
||||||
|
<td style={{ fontSize: "12px" }}>
|
||||||
|
{s.workspaceId ? (
|
||||||
|
<code>{s.workspaceId.slice(0, 8)}...</code>
|
||||||
|
) : "—"}
|
||||||
|
</td>
|
||||||
<td>{s.createdAt ? format(new Date(s.createdAt), "yyyy-MM-dd HH:mm") : "—"}</td>
|
<td>{s.createdAt ? format(new Date(s.createdAt), "yyyy-MM-dd HH:mm") : "—"}</td>
|
||||||
<td style={{ fontSize: "13px" }}>{s.ipAddress || "—"}</td>
|
<td style={{ fontSize: "13px" }}>{s.ipAddress || "—"}</td>
|
||||||
<td
|
<td
|
||||||
style={{ fontSize: "12px", maxWidth: "280px", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}
|
style={{ fontSize: "12px", maxWidth: "240px", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}
|
||||||
title={s.userAgent || ""}
|
title={s.userAgent || ""}
|
||||||
>
|
>
|
||||||
{s.userAgent || "—"}
|
{s.userAgent || "—"}
|
||||||
@ -143,7 +202,7 @@ export default function PlatformSessionsPage() {
|
|||||||
disabled={kicking === s.sessionId}
|
disabled={kicking === s.sessionId}
|
||||||
onClick={() => handleKickSession(s.sessionId)}
|
onClick={() => handleKickSession(s.sessionId)}
|
||||||
>
|
>
|
||||||
{kicking === s.sessionId ? "处理中..." : "下线"}
|
{kicking === s.sessionId ? "..." : "下线"}
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -157,3 +216,23 @@ export default function PlatformSessionsPage() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── AdminRpc Status Badge ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function AdminRpcUserStatus({ userId }: { userId: string }) {
|
||||||
|
const [status, setStatus] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getUserStatus(userId)
|
||||||
|
.then((r) => setStatus(r.status))
|
||||||
|
.catch(() => setStatus(null));
|
||||||
|
}, [userId]);
|
||||||
|
|
||||||
|
if (!status) return null;
|
||||||
|
const color = status === "Online" ? "#22c55e" : "#f59e0b";
|
||||||
|
return (
|
||||||
|
<span style={{ marginLeft: "8px", color, fontSize: "12px", fontWeight: 500 }}>
|
||||||
|
[{status}]
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -48,3 +48,8 @@ export const RUST_BACKEND_URL =
|
|||||||
process.env.RUST_BACKEND_URL || "http://localhost:3000";
|
process.env.RUST_BACKEND_URL || "http://localhost:3000";
|
||||||
export const ADMIN_API_SHARED_KEY =
|
export const ADMIN_API_SHARED_KEY =
|
||||||
process.env.ADMIN_API_SHARED_KEY || "";
|
process.env.ADMIN_API_SHARED_KEY || "";
|
||||||
|
|
||||||
|
// adminrpc HTTP 服务地址(k8s 内部默认地址)
|
||||||
|
// 在 Kubernetes 环境中默认使用 Service DNS,在本地开发时覆盖为 localhost:9091
|
||||||
|
export const ADMIN_RPC_URL =
|
||||||
|
process.env.ADMIN_RPC_URL || "http://adminrpc.admin.svc.cluster.local:9091";
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import {useEffect} from 'react';
|
import {useEffect} from 'react';
|
||||||
import {useNavigate} from 'react-router-dom';
|
import {useNavigate} from 'react-router-dom';
|
||||||
import {LandingNav} from '@/components/landing/landing-nav';
|
import {LandingNav} from '@/components/landing/landing-nav';
|
||||||
import {LandingHero, LandingFeatures, LandingHighlight} from '@/components/landing/landing-sections';
|
import {LandingFeatures, LandingHero, LandingHighlight} from '@/components/landing/landing-sections';
|
||||||
import {LandingFooter} from '@/components/landing/landing-footer';
|
import {LandingFooter} from '@/components/landing/landing-footer';
|
||||||
import {useUser} from '@/contexts/user-context';
|
import {useUser} from '@/contexts/user-context';
|
||||||
|
|
||||||
@ -9,25 +9,19 @@ export default function GitDataLandingPage() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const {isAuthenticated} = useUser();
|
const {isAuthenticated} = useUser();
|
||||||
const handleRegister = () => navigate('/auth/register');
|
const handleRegister = () => navigate('/auth/register');
|
||||||
|
|
||||||
// Redirect authenticated users to workspace
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
navigate('/w/me', {replace: true});
|
navigate('/w/me', {replace: true});
|
||||||
}
|
}
|
||||||
}, [isAuthenticated, navigate]);
|
}, [isAuthenticated, navigate]);
|
||||||
|
|
||||||
// Don't render landing page for authenticated users
|
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white dark:bg-zinc-950 text-zinc-900 dark:text-zinc-100 font-sans antialiased">
|
<div className="min-h-screen bg-white dark:bg-zinc-950 text-zinc-900 dark:text-zinc-100 font-sans antialiased">
|
||||||
{/* Subtle background grid */}
|
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 bg-[linear-gradient(to_right,#09090b08_1px,transparent_1px),linear-gradient(to_bottom,#09090b08_1px,transparent_1px)] bg-[size:24px_24px] pointer-events-none"/>
|
className="fixed inset-0 bg-[linear-gradient(to_right,#09090b08_1px,transparent_1px),linear-gradient(to_bottom,#09090b08_1px,transparent_1px)] bg-[size:24px_24px] pointer-events-none"/>
|
||||||
|
|
||||||
<LandingNav/>
|
<LandingNav/>
|
||||||
<main>
|
<main>
|
||||||
<LandingHero onRegister={handleRegister}/>
|
<LandingHero onRegister={handleRegister}/>
|
||||||
|
|||||||
@ -4360,6 +4360,7 @@ export type RoomAiResponse = {
|
|||||||
think: boolean;
|
think: boolean;
|
||||||
stream: boolean;
|
stream: boolean;
|
||||||
min_score?: number | null;
|
min_score?: number | null;
|
||||||
|
agent_type?: string | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
};
|
};
|
||||||
@ -4375,6 +4376,7 @@ export type RoomAiUpsertRequest = {
|
|||||||
think?: boolean | null;
|
think?: boolean | null;
|
||||||
stream?: boolean | null;
|
stream?: boolean | null;
|
||||||
min_score?: number | null;
|
min_score?: number | null;
|
||||||
|
agent_type?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RoomCategoryCreateRequest = {
|
export type RoomCategoryCreateRequest = {
|
||||||
|
|||||||
@ -55,6 +55,7 @@ export const RoomSettingsPanel = memo(function RoomSettingsPanel({
|
|||||||
const [useExact, setUseExact] = useState(false);
|
const [useExact, setUseExact] = useState(false);
|
||||||
const [think, setThink] = useState(false);
|
const [think, setThink] = useState(false);
|
||||||
const [stream, setStream] = useState(true);
|
const [stream, setStream] = useState(true);
|
||||||
|
const [agentType, setAgentType] = useState('chat');
|
||||||
const [isAddingAi, setIsAddingAi] = useState(false);
|
const [isAddingAi, setIsAddingAi] = useState(false);
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
@ -110,6 +111,7 @@ export const RoomSettingsPanel = memo(function RoomSettingsPanel({
|
|||||||
setUseExact(false);
|
setUseExact(false);
|
||||||
setThink(false);
|
setThink(false);
|
||||||
setStream(true);
|
setStream(true);
|
||||||
|
setAgentType('chat');
|
||||||
setShowAdvanced(false);
|
setShowAdvanced(false);
|
||||||
setShowAiAddDialog(true);
|
setShowAiAddDialog(true);
|
||||||
loadModels();
|
loadModels();
|
||||||
@ -131,6 +133,7 @@ export const RoomSettingsPanel = memo(function RoomSettingsPanel({
|
|||||||
use_exact: useExact,
|
use_exact: useExact,
|
||||||
think,
|
think,
|
||||||
stream,
|
stream,
|
||||||
|
agent_type: agentType || undefined,
|
||||||
};
|
};
|
||||||
await aiUpsert({
|
await aiUpsert({
|
||||||
path: { room_id: room.id },
|
path: { room_id: room.id },
|
||||||
@ -275,6 +278,14 @@ export const RoomSettingsPanel = memo(function RoomSettingsPanel({
|
|||||||
think
|
think
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{config.agent_type === 'react' && (
|
||||||
|
<span
|
||||||
|
className="rounded px-1 py-0.5 text-[10px] shrink-0"
|
||||||
|
style={{ background: 'rgba(168,85,247,0.15)', color: '#c084fc' }}
|
||||||
|
>
|
||||||
|
react
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@ -363,6 +374,32 @@ export const RoomSettingsPanel = memo(function RoomSettingsPanel({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Agent type */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-sm" style={{ color: 'var(--room-text)' }}>Agent Type</Label>
|
||||||
|
<Select value={agentType} onValueChange={(v) => { if (v !== null) setAgentType(v); }}>
|
||||||
|
<SelectTrigger className="w-full" style={{ background: 'var(--room-bg)', borderColor: 'var(--room-border)', color: 'var(--room-text)' }}>
|
||||||
|
<SelectValue>
|
||||||
|
{agentType === 'react' ? 'ReAct (multi-step reasoning)' : 'Chat (simple)'}
|
||||||
|
</SelectValue>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent style={{ background: 'var(--room-bg)', border: '1px solid var(--room-border)' }}>
|
||||||
|
<SelectItem value="chat">
|
||||||
|
<div className="flex flex-col items-start gap-0.5">
|
||||||
|
<span className="font-medium">Chat</span>
|
||||||
|
<span className="text-xs" style={{ color: 'var(--room-text-muted)' }}>Simple single-turn response</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="react">
|
||||||
|
<div className="flex flex-col items-start gap-0.5">
|
||||||
|
<span className="font-medium">ReAct</span>
|
||||||
|
<span className="text-xs" style={{ color: 'var(--room-text-muted)' }}>Multi-step reasoning with tool use</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Advanced settings toggle */}
|
{/* Advanced settings toggle */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user