Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 66 additions & 6 deletions artifacts/sandbox-ai/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,17 @@ import AuthCheck from "@/pages/auth-check";
import Templates from "@/pages/templates";
import ImportGitHub from "@/pages/import-github";
import { usePermissions } from "@/hooks/use-permissions";
import HealthConfigPage from "@/pages/health-config";
import { clerkConfig, isAuthEnabled } from "@/config/auth-mode";
import AuthPortal from "@/pages/auth-portal";
import MobileDashboard from "@/pages/mobile-dashboard";
import ChatStaticPage from "@/pages/chat-static";
import TermuxLab from "@/pages/termux-lab";
import ToolsHub from "@/pages/tools-hub";

const queryClient = new QueryClient();

const clerkPubKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY;
const clerkProxyUrl = import.meta.env.VITE_CLERK_PROXY_URL;
const { clerkPubKey, clerkProxyUrl } = clerkConfig;
const basePath = import.meta.env.BASE_URL.replace(/\/$/, "");

function stripBase(path: string): string {
Expand Down Expand Up @@ -198,15 +204,22 @@ function ClerkAuthTokenSetter() {
return null;
}


function Router() {
return (
<Switch>
<Route path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/get-app" component={GetApp} />
<Route path="/auth" component={AuthPortal} />
<Route path="/mobile-dashboard" component={MobileDashboard} />
<Route path="/chat-static" component={ChatStaticPage} />
<Route path="/termux-lab" component={TermuxLab} />
<Route path="/tools" component={ToolsHub} />
<Route path="/sign-in/*?" component={SignInPage} />
<Route path="/sign-up/*?" component={SignUpPage} />
<Route path="/auth-check" component={AuthCheck} />
<Route path="/health-config" component={HealthConfigPage} />
<Route path="/prompts">
<ProtectedRoute><Prompts /></ProtectedRoute>
</Route>
Expand Down Expand Up @@ -258,14 +271,61 @@ function ClerkProviderWithRoutes() {
);
}


function PublicOnlyRouter() {
return (
<Switch>
<Route path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/pricing">
<Redirect to="/" />
</Route>
<Route path="/get-app" component={GetApp} />
<Route path="/auth" component={AuthPortal} />
<Route path="/mobile-dashboard" component={MobileDashboard} />
<Route path="/chat-static" component={ChatStaticPage} />
<Route path="/termux-lab" component={TermuxLab} />
<Route path="/tools" component={ToolsHub} />
<Route path="/download" component={Download} />
<Route path="/health-config" component={HealthConfigPage} />
<Route path="/sign-in/*?">
<Redirect to="/auth" />
</Route>
<Route path="/sign-up/*?">
<Redirect to="/auth" />
</Route>
<Route path="/chat">
<Redirect to="/" />
</Route>
<Route path="/chat/:id">
<Redirect to="/" />
</Route>
<Route component={NotFound} />
</Switch>
);
}

function PublicAppWithoutAuth() {
return (
<QueryClientProvider client={queryClient}>
<TooltipProvider>
<div style={{ color: "#fff", padding: 16, fontSize: 14, opacity: 0.85 }}>
Auth is running in public mode. Open <code>/health-config</code> to inspect environment readiness.
</div>
<PublicOnlyRouter />
<Toaster />
</TooltipProvider>
</QueryClientProvider>
);
}
function App() {
const [showSplash, setShowSplash] = useState(true);

if (!clerkPubKey) {
if (!isAuthEnabled) {
return (
<div style={{ color: "#fff", padding: 24 }}>
Missing VITE_CLERK_PUBLISHABLE_KEY — please check environment variables.
</div>
<WouterRouter base={basePath}>
<PublicAppWithoutAuth />
</WouterRouter>
);
}

Expand Down
2 changes: 1 addition & 1 deletion artifacts/sandbox-ai/src/components/chat-area.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ export function ChatArea({ conversationId, onToggleSidebar, isSidebarOpen }: Cha
createConversation.mutate(
{ data: { title: content.slice(0, 60), mode: mode as any, model } },
{
onSuccess: (conv) => {
onSuccess: (conv: any) => {
queryClient.invalidateQueries({ queryKey: getListOpenaiConversationsQueryKey() });
pendingRef.current = { message: content, forConvId: conv.id };
navigate(`/chat/${conv.id}`);
Expand Down
4 changes: 2 additions & 2 deletions artifacts/sandbox-ai/src/components/chat-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export function ChatSidebar({ isOpen, onToggle, activeId, isMobile = false, onCl
createConversation.mutate(
{ data: { title: "New Conversation", mode: "chat", model: "gpt-5.2" } },
{
onSuccess: (conv) => {
onSuccess: (conv: any) => {
queryClient.invalidateQueries({ queryKey: getListOpenaiConversationsQueryKey() });
setLocation(`/chat/${conv.id}`);
onClose?.();
Expand All @@ -130,7 +130,7 @@ export function ChatSidebar({ isOpen, onToggle, activeId, isMobile = false, onCl
if (!conversations) return [];
if (!search.trim()) return conversations;
const q = search.toLowerCase();
return conversations.filter((c) => c.title?.toLowerCase().includes(q) || c.mode?.toLowerCase().includes(q));
return conversations.filter((c: any) => c.title?.toLowerCase().includes(q) || c.mode?.toLowerCase().includes(q));
}, [conversations, search]);

const grouped = useMemo(() => groupConversations(filtered as any), [filtered]);
Expand Down
38 changes: 38 additions & 0 deletions artifacts/sandbox-ai/src/config/auth-mode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export type AuthMode = "clerk" | "public";

const clerkPubKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY;
const clerkProxyUrl = import.meta.env.VITE_CLERK_PROXY_URL;
const requestedAuthMode = (import.meta.env.VITE_AUTH_MODE ?? "clerk").toLowerCase();

export const authMode: AuthMode = requestedAuthMode === "public" ? "public" : "clerk";
export const isClerkConfigured = Boolean(clerkPubKey);
export const isAuthEnabled = authMode === "clerk" && isClerkConfigured;

export const clerkConfig = {
clerkPubKey,
clerkProxyUrl,
};

export const envChecklist = [
{
key: "VITE_AUTH_MODE",
value: import.meta.env.VITE_AUTH_MODE,
required: false,
status: "info" as const,
hint: "Optional. Use 'clerk' (default) or 'public'.",
},
{
key: "VITE_CLERK_PUBLISHABLE_KEY",
value: clerkPubKey,
required: authMode === "clerk",
status: isClerkConfigured ? ("ok" as const) : ("missing" as const),
hint: "Required when VITE_AUTH_MODE=clerk.",
},
{
key: "VITE_CLERK_PROXY_URL",
value: clerkProxyUrl,
required: false,
status: "info" as const,
hint: "Optional proxy URL for Clerk.",
},
];
18 changes: 18 additions & 0 deletions artifacts/sandbox-ai/src/pages/auth-portal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Link } from "wouter";

export default function AuthPortal() {
return (
<div className="min-h-screen bg-black text-white p-6">
<div className="max-w-md mx-auto pt-12 space-y-6">
<h1 className="text-3xl font-black">Đăng nhập / Đăng ký</h1>
<p className="text-sm text-white/70">
Quy trình OAuth (Google/GitHub) được xử lý qua Clerk. Bấm nút bên dưới để vào trang xác thực chính thức.
</p>
<div className="grid gap-3">
<Link href="/sign-in" className="rounded-xl px-4 py-3 bg-cyan-400 text-black font-bold text-center">Tiếp tục với Google / GitHub</Link>
<Link href="/sign-up" className="rounded-xl px-4 py-3 border border-white/20 font-semibold text-center">Tạo tài khoản mới</Link>
</div>
</div>
</div>
);
}
22 changes: 22 additions & 0 deletions artifacts/sandbox-ai/src/pages/chat-static.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export default function ChatStaticPage() {
const messages = [
{ role: "user", content: "Tạo API login bằng Node.js" },
{ role: "assistant", content: "Dưới đây là skeleton theo phong cách Codex, gồm route, service và validator..." },
];
return (
<div className="min-h-screen bg-[#0b0d12] text-white flex flex-col">
<header className="p-4 border-b border-white/10 font-black">Codex-style Chat (Static UI)</header>
<div className="flex-1 p-4 space-y-4">
{messages.map((m, i) => (
<div key={i} className={`max-w-3xl rounded-xl p-3 ${m.role === "user" ? "ml-auto bg-cyan-500/20" : "bg-white/5"}`}>
<div className="text-xs opacity-70 mb-1">{m.role}</div>
<div>{m.content}</div>
</div>
))}
</div>
<div className="p-4 border-t border-white/10">
<input disabled value="Static mock input..." className="w-full rounded-lg bg-white/10 px-3 py-2 text-sm" />
</div>
</div>
);
}
27 changes: 27 additions & 0 deletions artifacts/sandbox-ai/src/pages/health-config.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { authMode, envChecklist, isAuthEnabled } from "@/config/auth-mode";

export default function HealthConfigPage() {
return (
<div className="min-h-screen" style={{ backgroundColor: "var(--sb-bg)", color: "#fff", padding: 24 }}>
<h1 style={{ fontSize: 28, fontWeight: 800, marginBottom: 12 }}>Health Config</h1>
<p style={{ opacity: 0.8, marginBottom: 20 }}>
Runtime auth mode: <strong>{authMode}</strong>. Auth enabled: <strong>{String(isAuthEnabled)}</strong>.
</p>
<div style={{ display: "grid", gap: 10 }}>
{envChecklist.map((item) => (
<div key={item.key} style={{ border: "1px solid rgba(255,255,255,0.15)", borderRadius: 10, padding: 12 }}>
<div style={{ display: "flex", justifyContent: "space-between", gap: 12 }}>
<code>{item.key}</code>
<strong>
{item.status === "ok" ? "✅ configured" : item.status === "missing" ? "❌ missing" : "ℹ️ optional"}
</strong>
</div>
<div style={{ marginTop: 6, fontSize: 13, opacity: 0.85 }}>required: {String(item.required)}</div>
<div style={{ marginTop: 6, fontSize: 13, opacity: 0.85 }}>value: {item.value ? "set" : "empty"}</div>
<div style={{ marginTop: 6, fontSize: 13, opacity: 0.85 }}>{item.hint}</div>
</div>
))}
</div>
</div>
);
}
36 changes: 36 additions & 0 deletions artifacts/sandbox-ai/src/pages/mobile-dashboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useState } from "react";
import { Link } from "wouter";
import { Menu, X, MessageSquare, Wrench, TerminalSquare, User } from "lucide-react";

export default function MobileDashboard() {
const [open, setOpen] = useState(false);
return (
<div className="min-h-screen bg-[#06070a] text-white">
<header className="sticky top-0 z-20 border-b border-white/10 bg-black/60 backdrop-blur px-4 py-3 flex items-center justify-between">
<button onClick={() => setOpen((v) => !v)} className="p-2 rounded-lg border border-white/20">
Comment thread
Huynhthuongg marked this conversation as resolved.
{open ? <X size={18} /> : <Menu size={18} />}
</button>
<h1 className="font-black">Sandbox Dashboard</h1>
<User size={18} className="opacity-70" />
</header>

{open && (
<nav className="p-4 space-y-2 border-b border-white/10">
<Link href="/chat-static" className="flex items-center gap-2 p-3 rounded-lg bg-white/5"><MessageSquare size={16}/> Chat tĩnh (Codex style)</Link>
<Link href="/termux-lab" className="flex items-center gap-2 p-3 rounded-lg bg-white/5"><TerminalSquare size={16}/> Termux nền + AI lệnh</Link>
<Link href="/tools" className="flex items-center gap-2 p-3 rounded-lg bg-white/5"><Wrench size={16}/> Công cụ hỗ trợ</Link>
</nav>
)}

<main className="p-4 space-y-3">
<div className="rounded-2xl border border-cyan-400/30 bg-cyan-400/10 p-4">
<p className="text-sm text-cyan-200">Mobile-first layout với menu 3 gạch như bạn yêu cầu.</p>
</div>
<div className="grid grid-cols-2 gap-3">
<Link href="/chat-static" className="rounded-xl p-4 bg-white/5 border border-white/10">Mở Chat</Link>
<Link href="/tools" className="rounded-xl p-4 bg-white/5 border border-white/10">Mở Tools</Link>
</div>
</main>
</div>
);
}
18 changes: 18 additions & 0 deletions artifacts/sandbox-ai/src/pages/termux-lab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useState } from "react";

export default function TermuxLab() {
const [cmd, setCmd] = useState("npm run build");
const [logs, setLogs] = useState<string[]>(["[termux] session booted", "[ai] ready to suggest commands"]);
return (
<div className="min-h-screen bg-black text-green-300 p-4 font-mono">
<h1 className="text-white font-bold mb-4">Termux nền + AI command assistant (mock)</h1>
<div className="rounded-xl border border-green-500/30 p-3 mb-3 bg-green-950/20">
{logs.map((l, i) => <div key={i}>{l}</div>)}
</div>
<div className="flex gap-2">
<input value={cmd} onChange={(e) => setCmd(e.target.value)} className="flex-1 bg-white/10 text-white px-3 py-2 rounded" aria-label="Command" />
<button onClick={() => setLogs((v) => [...v, `$ ${cmd}`, "[ai] suggestion: add --verbose if fail"])} className="px-3 py-2 rounded bg-cyan-500 text-black font-bold">Run</button>
</div>
</div>
);
}
20 changes: 20 additions & 0 deletions artifacts/sandbox-ai/src/pages/tools-hub.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const tools = [
"Prompt Optimizer",
"API Schema Checker",
"Log Analyzer",
"Release Checklist",
"Env Validator",
];

export default function ToolsHub() {
return (
<div className="min-h-screen bg-[#080a10] text-white p-6">
<h1 className="text-2xl font-black mb-4">Công cụ hỗ trợ</h1>
<div className="grid gap-3">
{tools.map((t) => (
<div key={t} className="rounded-xl border border-white/15 p-4 bg-white/5">{t}</div>
))}
</div>
</div>
);
}
26 changes: 21 additions & 5 deletions artifacts/sandbox-ai/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,33 @@
{
"extends": "../../tsconfig.base.json",
"include": ["src/**/*"],
"exclude": ["node_modules", "build", "dist", "**/*.test.ts"],
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"build",
"dist",
"**/*.test.ts"
],
"compilerOptions": {
"noEmit": true,
"jsx": "preserve",
"lib": ["esnext", "dom", "dom.iterable"],
"lib": [
"esnext",
"dom",
"dom.iterable"
],
"resolveJsonModule": true,
"allowImportingTsExtensions": true,
"moduleResolution": "bundler",
"types": ["node", "vite/client"],
"types": [
"node",
"vite/client"
],
"paths": {
"@/*": ["./src/*"]
"@/*": [
"./src/*"
]
}
},
"references": [
Expand Down
27 changes: 27 additions & 0 deletions docs_self_healing_process.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Self-healing reminder workflow (OpenClaw/Eios style)

## Goal
Automatically remind maintainers when deployment-critical configuration or code artifacts are missing.

## 1) Detect
- On every deploy, open `/health-config` and validate required env keys.
- On CI, run `pnpm --filter @workspace/sandbox-ai run build` and fail on errors.

## 2) Classify
- **Config missing** (e.g. `VITE_CLERK_PUBLISHABLE_KEY` when `VITE_AUTH_MODE=clerk`).
- **Artifact missing** (route/page/module referenced but not found).
- **Type/build breakage** (TypeScript/build errors).

## 3) Notify owner
- Config missing -> notify DevOps/release owner.
- Artifact missing -> notify feature owner/repo maintainer.
- Build/type breakage -> notify author of latest PR and reviewer.

## 4) Auto-remediation checklist
- If auth env missing in non-production: set `VITE_AUTH_MODE=public` as temporary fallback.
- If auth env missing in production: block release and require secret injection.
- If artifact missing: create placeholder file + TODO with owner and deadline.

## 5) Prevent regressions
- Add a PR checklist item: “Did you verify `/health-config` and auth mode behavior?”
- Keep a short runbook for required variables per environment.