Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
9 changes: 9 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ COPY public/ public/
COPY skills-builtin/ skills-builtin/
COPY tsconfig.json biome.json ./

# --- Chat UI Build Stage ---
FROM oven/bun:1 AS chat-ui-builder
WORKDIR /app/chat-ui
COPY chat-ui/package.json chat-ui/bun.lock* ./
RUN bun install --frozen-lockfile
COPY chat-ui/ ./
RUN bun run build

# --- Runtime Stage ---
FROM oven/bun:1-slim
WORKDIR /app
Expand Down Expand Up @@ -73,6 +81,7 @@ COPY --from=builder /app/src ./src
COPY --from=builder /app/config ./config
COPY --from=builder /app/scripts ./scripts
COPY --from=builder /app/public ./public
COPY --from=chat-ui-builder /app/chat-ui/dist ./public/chat
COPY --from=builder /app/skills-builtin ./skills-builtin
COPY --from=builder /app/package.json ./
COPY --from=builder /app/tsconfig.json ./
Expand Down
3 changes: 3 additions & 0 deletions chat-ui/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
dist
.vite
525 changes: 525 additions & 0 deletions chat-ui/bun.lock

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions chat-ui/components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/styles/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
28 changes: 28 additions & 0 deletions chat-ui/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="light dark" />
<title>Phantom</title>
<link rel="icon" href="/chat/favicon.svg" type="image/svg+xml" />
<link
rel="preload"
href="/chat/fonts/inter-var-latin.woff2"
as="font"
type="font/woff2"
crossorigin
/>
<link
rel="preload"
href="/chat/fonts/jetbrains-mono-var-latin.woff2"
as="font"
type="font/woff2"
crossorigin
/>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
35 changes: 35 additions & 0 deletions chat-ui/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "phantom-chat-ui",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"typecheck": "tsc -b"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"lucide-react": "^1.8.0",
"next-themes": "^0.4.6",
"radix-ui": "^1.4.3",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.5.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.0"
},
"devDependencies": {
"vite": "^6.3.0",
"@vitejs/plugin-react": "^4.4.0",
"typescript": "^5.7.0",
"@types/react": "^19.1.0",
"@types/react-dom": "^19.1.0",
"@types/node": "^22.15.0"
}
}
6 changes: 6 additions & 0 deletions chat-ui/public/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions chat-ui/public/fonts/instrument-serif-latin.woff2
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang=en>
<meta charset=utf-8>
<meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
<title>Error 404 (Not Found)!!1</title>
<style>
*{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px}
</style>
<a href=//www.google.com/><span id=logo aria-label=Google></span></a>
<p><b>404.</b> <ins>That’s an error.</ins>
<p>The requested URL <code>/s/instrumentserif/v4/jizBRFtNs2ka5fXjeivQ4LroWlx-2zIZjw.woff2</code> was not found on this server. <ins>That’s all we know.</ins>
11 changes: 11 additions & 0 deletions chat-ui/public/fonts/inter-var-latin.woff2
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang=en>
<meta charset=utf-8>
<meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
<title>Error 404 (Not Found)!!1</title>
<style>
*{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px}
</style>
<a href=//www.google.com/><span id=logo aria-label=Google></span></a>
<p><b>404.</b> <ins>That’s an error.</ins>
<p>The requested URL <code>/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjQ.woff2</code> was not found on this server. <ins>That’s all we know.</ins>
11 changes: 11 additions & 0 deletions chat-ui/public/fonts/jetbrains-mono-var-latin.woff2
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang=en>
<meta charset=utf-8>
<meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
<title>Error 404 (Not Found)!!1</title>
<style>
*{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px}
</style>
<a href=//www.google.com/><span id=logo aria-label=Google></span></a>
<p><b>404.</b> <ins>That’s an error.</ins>
<p>The requested URL <code>/s/jetbrainsmono/v18/tDbY2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKxjPVmUsaaDhw.woff2</code> was not found on this server. <ins>That’s all we know.</ins>
17 changes: 17 additions & 0 deletions chat-ui/public/manifest.webmanifest
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "Phantom",
"short_name": "Phantom",
"description": "Phantom AI co-worker",
"start_url": "/chat/",
"scope": "/chat/",
"display": "standalone",
"background_color": "#faf9f5",
"theme_color": "#4850c4",
"icons": [
{
"src": "/chat/favicon.svg",
"sizes": "any",
"type": "image/svg+xml"
}
]
}
46 changes: 46 additions & 0 deletions chat-ui/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Suspense } from "react";
import {
createBrowserRouter,
Outlet,
RouterProvider,
} from "react-router-dom";
import { AppShell } from "@/components/app-shell";
import { ChatRoute } from "@/routes/chat-route";
import { NewChatRoute } from "@/routes/new-chat-route";
import { NotFoundRoute } from "@/routes/not-found-route";
import { SessionRoute } from "@/routes/session-route";

function Layout() {
return (
<AppShell>
<Suspense
fallback={
<div className="flex h-full items-center justify-center">
<div className="h-5 w-5 animate-spin rounded-full border-2 border-muted-foreground border-t-transparent" />
</div>
}
>
<Outlet />
</Suspense>
</AppShell>
);
}

const router = createBrowserRouter(
[
{
element: <Layout />,
children: [
{ index: true, element: <ChatRoute /> },
{ path: "s/:sessionId", element: <SessionRoute /> },
{ path: "new", element: <NewChatRoute /> },
{ path: "*", element: <NotFoundRoute /> },
],
},
],
{ basename: "/chat" },
);

export function App() {
return <RouterProvider router={router} />;
}
128 changes: 128 additions & 0 deletions chat-ui/src/components/app-shell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { useCallback, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { useKeyboard } from "@/hooks/use-keyboard";
import { useSessions } from "@/hooks/use-sessions";
import { useTheme } from "@/hooks/use-theme";
import { useIsMobile } from "@/hooks/use-mobile";
import { CommandPalette } from "./command-palette";
import { DeleteSessionDialog } from "./delete-session-dialog";
import { SidebarPanel } from "./sidebar-panel";

export function AppShell({ children }: { children: React.ReactNode }) {
const navigate = useNavigate();
const { sessionId } = useParams<{ sessionId: string }>();
const { sessions, isLoading, createSession, deleteSession, updateSession } =
useSessions();
const { toggleTheme } = useTheme();
const isMobile = useIsMobile();

const [sidebarOpen, setSidebarOpen] = useState(!isMobile);
const [deleteTarget, setDeleteTarget] = useState<{
id: string;
title: string | null;
} | null>(null);

const handleNewSession = useCallback(async () => {
const id = await createSession();
navigate(`/s/${id}`);
}, [createSession, navigate]);

const handleSessionClick = useCallback(
(id: string) => {
navigate(`/s/${id}`);
if (isMobile) setSidebarOpen(false);
},
[navigate, isMobile],
);

const handleRename = useCallback(
(id: string, title: string) => {
updateSession(id, { title });
},
[updateSession],
);

const handleDeleteRequest = useCallback(
(id: string) => {
const session = sessions.find((s) => s.id === id);
setDeleteTarget({ id, title: session?.title ?? null });
},
[sessions],
);

const handleDeleteConfirm = useCallback(() => {
if (!deleteTarget) return;
deleteSession(deleteTarget.id);
setDeleteTarget(null);
if (deleteTarget.id === sessionId) {
navigate("/");
}
}, [deleteTarget, deleteSession, sessionId, navigate]);

useKeyboard({
newSession: handleNewSession,
toggleTheme,
});

return (
<div className="flex h-dvh overflow-hidden bg-background">
{sidebarOpen && (
<div className="w-64 shrink-0 border-r border-border">
<SidebarPanel
sessions={sessions}
isLoading={isLoading}
activeSessionId={sessionId ?? null}
onSessionClick={handleSessionClick}
onNewSession={handleNewSession}
onRename={handleRename}
onDelete={handleDeleteRequest}
/>
</div>
)}

<div className="flex min-w-0 flex-1 flex-col">
<header className="flex h-12 items-center border-b border-border px-4">
<button
type="button"
onClick={() => setSidebarOpen(!sidebarOpen)}
className="mr-3 text-muted-foreground hover:text-foreground"
aria-label={sidebarOpen ? "Close sidebar" : "Open sidebar"}
>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
className="h-4 w-4"
>
<path
d="M2 4h12M2 8h12M2 12h12"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
/>
</svg>
</button>
<span className="text-sm font-medium text-foreground">Phantom</span>
</header>

<main className="flex min-h-0 flex-1 flex-col">{children}</main>
</div>

<CommandPalette
sessions={sessions}
onNewSession={handleNewSession}
onSessionClick={handleSessionClick}
/>

<DeleteSessionDialog
open={deleteTarget !== null}
onOpenChange={(open) => {
if (!open) setDeleteTarget(null);
}}
onConfirm={handleDeleteConfirm}
sessionTitle={deleteTarget?.title ?? null}
/>
</div>
);
}
Loading
Loading